brick 1.0.18 → 1.0.21

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: add7e232024e140e2fb089ef158600e2a2a955efd787dc986769b43b4b1f6d24
4
- data.tar.gz: a45d187a7b739f90b1fede46dfc9f63a2057a3410d052e0ad57322f4741ebfee
3
+ metadata.gz: 0ad289d9f4db9ef6ce9edcfb8948926d4a61910294291272ccf2b342fa2f5051
4
+ data.tar.gz: 9030f9ccf39d7f0a9a0ff9413e4f24ee7a46beaf3aba056e9875fc5c29551419
5
5
  SHA512:
6
- metadata.gz: '0897e041c617f0e1e695d0402784184ce2f7ec575d9b0e7a7b1b59b2efbcb40a4de28bcbe2eb124f576dddb8e7371f572c67ee3b19120cd281bf3358249b4508'
7
- data.tar.gz: fc7a4a93a7d2409f4aa82fb3dbf23710aaad45a1dca1193c8cbcefa86d5d9061a773214c8fe0dd02375660bfb6e5a9856df3a202f57a73990e79d23850918f9b
6
+ metadata.gz: ed6f08eabba91cb304141a0c84884cfbeaae0087094b1fe1ff4a80195aee44dbb697c385ab9d7afaac2e337f9008bc7bef059be98f95264fec34a2288beaff6b
7
+ data.tar.gz: 5ebbb793d23634232a529b3d39224e1e4deaa010cf8bf2841effe20a9e3b4d7079fb7163e6c2f90feeed8526c4a1314f319bad47d1c8a6079bf54ca92f0e6224
data/lib/brick/config.rb CHANGED
@@ -74,6 +74,20 @@ module Brick
74
74
  @mutex.synchronize { @exclude_hms = skips }
75
75
  end
76
76
 
77
+ # Skip showing counts for these specific has_many associations when building auto-generated #index views
78
+ def skip_index_hms
79
+ @mutex.synchronize { @skip_index_hms || {} }
80
+ end
81
+
82
+ def skip_index_hms=(skips)
83
+ @mutex.synchronize do
84
+ @skip_index_hms ||= skips.each_with_object({}) do |v, s|
85
+ class_name, assoc_name = v.split('.')
86
+ (s[class_name] ||= {})[assoc_name.to_sym] = nil
87
+ end
88
+ end
89
+ end
90
+
77
91
  # Associations to treat as a has_one
78
92
  def has_ones
79
93
  @mutex.synchronize { @has_ones }
@@ -32,32 +32,101 @@
32
32
 
33
33
  # Currently quadrupling up routes
34
34
 
35
+
36
+ # From the North app:
37
+ # undefined method `built_in_role_path' when referencing show on a subclassed STI:
38
+ # http://localhost:3000/roles/3?_brick_schema=cust1
39
+
40
+
35
41
  # ==========================================================
36
42
  # Dynamically create model or controller classes when needed
37
43
  # ==========================================================
38
44
 
39
45
  # By default all models indicate that they are not views
46
+ module Arel
47
+ class Table
48
+ def _arel_table_type
49
+ # AR < 4.2 doesn't have type_caster at all, so rely on an instance variable getting set
50
+ # AR 4.2 - 5.1 have buggy type_caster entries for the root node
51
+ instance_variable_get(:@_arel_table_type) ||
52
+ # 5.2-7.0 does type_caster just fine, no bugs there, but the property with the type differs:
53
+ # 5.2 has "types" as public, 6.0 "types" as private, and >= 6.1 "klass" as private.
54
+ ((tc = send(:type_caster)) && tc.instance_variable_get(:@types)) ||
55
+ tc.send(:klass)
56
+ end
57
+ end
58
+ end
59
+
40
60
  module ActiveRecord
41
61
  class Base
62
+ def self._assoc_names
63
+ @_assoc_names ||= {}
64
+ end
65
+
42
66
  def self.is_view?
43
67
  false
44
68
  end
45
69
 
46
70
  # Used to show a little prettier name for an object
47
- def brick_descrip
48
- klass = self.class
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
-
71
+ def self.brick_get_dsl
53
72
  # 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 -
73
+ unless (dsl = ::Brick.config.model_descrips[name])
74
+ descrip_col = (columns.map(&:name) - _brick_get_fks -
56
75
  (::Brick.config.metadata_columns || []) -
57
- [klass.primary_key]).first
58
- ::Brick.config.model_descrips[klass.name] = "[#{descrip_col}]" if descrip_col
76
+ [primary_key]).first
77
+ dsl = ::Brick.config.model_descrips[name] = "[#{descrip_col}]" if descrip_col
78
+ end
79
+ dsl
80
+ end
81
+
82
+ # Pass in true or a JoinArray
83
+ def self.brick_parse_dsl(build_array = nil, prefix = [], translations = {})
84
+ build_array = ::Brick::JoinArray.new.tap { |ary| ary.replace([build_array]) } if build_array.is_a?(::Brick::JoinHash)
85
+ build_array = ::Brick::JoinArray.new unless build_array.nil? || build_array.is_a?(Array)
86
+ members = []
87
+ bracket_name = nil
88
+ prefix = [prefix] unless prefix.is_a?(Array)
89
+ if (dsl = ::Brick.config.model_descrips[name] || brick_get_dsl)
90
+ klass = nil
91
+ dsl.each_char do |ch|
92
+ if bracket_name
93
+ if ch == ']' # Time to process a bracketed thing?
94
+ parts = bracket_name.split('.')
95
+ first_parts = parts[0..-2].map { |part| klass = klass.reflect_on_association(part_sym = part.to_sym).klass; part_sym }
96
+ parts = prefix + first_parts + [parts[-1]]
97
+ if parts.length > 1
98
+ s = build_array
99
+ parts[0..-3].each { |v| s = s[v.to_sym] }
100
+ s[parts[-2]] = nil # unless parts[-2].empty? # Using []= will "hydrate" any missing part(s) in our whole series
101
+ translations[parts[0..-2].join('.')] = klass
102
+ end
103
+ members << parts
104
+ bracket_name = nil
105
+ else
106
+ bracket_name << ch
107
+ end
108
+ elsif ch == '['
109
+ bracket_name = +''
110
+ klass = self
111
+ end
112
+ end
113
+ else # With no DSL available, still put this prefix into the JoinArray so we can get primary key (ID) info from this table
114
+ x = prefix.each_with_object(build_array) { |v, s| s[v.to_sym] }
115
+ x[prefix[-1]] = nil unless prefix.empty? # Using []= will "hydrate" any missing part(s) in our whole series
59
116
  end
60
- if (dsl ||= ::Brick.config.model_descrips[klass.name])
117
+ members
118
+ end
119
+
120
+ # If available, parse simple DSL attached to a model in order to provide a friendlier name.
121
+ # Object property names can be referenced in square brackets like this:
122
+ # { 'User' => '[profile.firstname] [profile.lastname]' }
123
+ def brick_descrip
124
+ self.class.brick_descrip(self)
125
+ end
126
+
127
+ def self.brick_descrip(obj, data = nil, pk_alias = nil)
128
+ if (dsl = ::Brick.config.model_descrips[(klass = self).name] || klass.brick_get_dsl)
129
+ idx = -1
61
130
  caches = {}
62
131
  output = +''
63
132
  is_brackets_have_content = false
@@ -65,18 +134,23 @@ module ActiveRecord
65
134
  dsl.each_char do |ch|
66
135
  if bracket_name
67
136
  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]
137
+ datum = if data
138
+ data[idx += 1].to_s
74
139
  else
75
- (caches[obj_name] = obj&.send(part.to_sym))
140
+ obj_name = +''
141
+ this_obj = obj
142
+ bracket_name.split('.').each do |part|
143
+ obj_name += ".#{part}"
144
+ this_obj = if caches.key?(obj_name)
145
+ caches[obj_name]
146
+ else
147
+ (caches[obj_name] = this_obj&.send(part.to_sym))
148
+ end
149
+ end
150
+ this_obj&.to_s || ''
76
151
  end
77
- end
78
- is_brackets_have_content = true unless (obj&.to_s).blank?
79
- output << (obj&.to_s || '')
152
+ is_brackets_have_content = true unless (datum).blank?
153
+ output << (datum || '')
80
154
  bracket_name = nil
81
155
  else
82
156
  bracket_name << ch
@@ -91,10 +165,14 @@ module ActiveRecord
91
165
  end
92
166
  if is_brackets_have_content
93
167
  output
94
- elsif klass.primary_key
95
- "#{klass.name} ##{send(klass.primary_key)}"
168
+ elsif pk_alias
169
+ if (id = obj.send(pk_alias))
170
+ "#{klass.name} ##{id}"
171
+ end
172
+ # elsif klass.primary_key
173
+ # "#{klass.name} ##{obj.send(klass.primary_key)}"
96
174
  else
97
- to_s
175
+ obj.to_s
98
176
  end
99
177
  end
100
178
 
@@ -106,9 +184,91 @@ module ActiveRecord
106
184
  end
107
185
 
108
186
  class Relation
109
- def brick_where(params)
187
+ attr_reader :_brick_chains
188
+
189
+ # CLASS STUFF
190
+ def _recurse_arel(piece, prefix = '')
191
+ names = []
192
+ # Our JOINs mashup of nested arrays and hashes
193
+ # binding.pry if defined?(@arel)
194
+ case piece
195
+ when Array
196
+ names += piece.inject([]) { |s, v| s + _recurse_arel(v, prefix) }
197
+ when Hash
198
+ names += piece.inject([]) do |s, v|
199
+ new_prefix = "#{prefix}#{v.first}_"
200
+ s << [v.last.shift, new_prefix]
201
+ s + _recurse_arel(v.last, new_prefix)
202
+ end
203
+
204
+ # ActiveRecord AREL objects
205
+ when Arel::Nodes::Join # INNER or OUTER JOIN
206
+ # rubocop:disable Style/IdenticalConditionalBranches
207
+ if piece.right.is_a?(Arel::Table) # Came in from AR < 3.2?
208
+ # Arel 2.x and older is a little curious because these JOINs work "back to front".
209
+ # The left side here is either another earlier JOIN, or at the end of the whole tree, it is
210
+ # the first table.
211
+ names += _recurse_arel(piece.left)
212
+ # The right side here at the top is the very last table, and anywhere else down the tree it is
213
+ # the later "JOIN" table of this pair. (The table that comes after all the rest of the JOINs
214
+ # from the left side.)
215
+ names << [piece.right._arel_table_type, (piece.right.table_alias || piece.right.name)]
216
+ else # "Normal" setup, fed from a JoinSource which has an array of JOINs
217
+ # The left side is the "JOIN" table
218
+ names += _recurse_arel(piece.left)
219
+ # The expression on the right side is the "ON" clause
220
+ # on = piece.right.expr
221
+ # # Find the table which is not ourselves, and thus must be the "path" that led us here
222
+ # parent = piece.left == on.left.relation ? on.right.relation : on.left.relation
223
+ # binding.pry if piece.left.is_a?(Arel::Nodes::TableAlias)
224
+ table = piece.left
225
+ if table.is_a?(Arel::Nodes::TableAlias)
226
+ alias_name = table.right
227
+ table = table.left
228
+ end
229
+ (_brick_chains[table._arel_table_type] ||= []) << (alias_name || table.table_alias || table.name)
230
+ # puts "YES! #{self.object_id}"
231
+ end
232
+ # rubocop:enable Style/IdenticalConditionalBranches
233
+ when Arel::Table # Table
234
+ names << [piece._arel_table_type, (piece.table_alias || piece.name)]
235
+ when Arel::Nodes::TableAlias # Alias
236
+ # Can get the real table name from: self._recurse_arel(piece.left)
237
+ names << [piece.left._arel_table_type, piece.right.to_s] # This is simply a string; the alias name itself
238
+ when Arel::Nodes::JoinSource # Leaving this until the end because AR < 3.2 doesn't know at all about JoinSource!
239
+ # Spin up an empty set of Brick alias name chains at the start
240
+ @_brick_chains = {}
241
+ # The left side is the "FROM" table
242
+ # names += _recurse_arel(piece.left)
243
+ names << [piece.left._arel_table_type, (piece.left.table_alias || piece.left.name)]
244
+ # The right side is an array of all JOINs
245
+ piece.right.each { |join| names << _recurse_arel(join) }
246
+ end
247
+ names
248
+ end
249
+
250
+ # INSTANCE STUFF
251
+ def _arel_alias_names
252
+ # %%% If with Rails 3.1 and older you get "NoMethodError: undefined method `eq' for nil:NilClass"
253
+ # when trying to call relation.arel, then somewhere along the line while navigating a has_many
254
+ # relationship it can't find the proper foreign key.
255
+ core = arel.ast.cores.first
256
+ # Accommodate AR < 3.2
257
+ if core.froms.is_a?(Arel::Table)
258
+ # All recent versions of AR have #source which brings up an Arel::Nodes::JoinSource
259
+ _recurse_arel(core.source)
260
+ else
261
+ # With AR < 3.2, "froms" brings up the top node, an Arel::Nodes::InnerJoin
262
+ _recurse_arel(core.froms)
263
+ end
264
+ end
265
+
266
+ def brick_select(params, selects = nil, bt_descrip = {}, hm_counts = {}, join_array = ::Brick::JoinArray.new
267
+ # , is_add_bts, is_add_hms
268
+ )
269
+ is_add_bts = is_add_hms = true
110
270
  wheres = {}
111
- rel_joins = []
271
+ has_hm = false
112
272
  params.each do |k, v|
113
273
  case (ks = k.split('.')).length
114
274
  when 1
@@ -116,28 +276,93 @@ module ActiveRecord
116
276
  when 2
117
277
  assoc_name = ks.first.to_sym
118
278
  # Make sure it's a good association name and that the model has that column name
119
- next unless klass.reflect_on_association(assoc_name)&.klass&.columns&.map(&:name)&.include?(ks.last)
120
-
121
- rel_joins << assoc_name unless rel_joins.include?(assoc_name)
279
+ next unless (assoc = klass.reflect_on_association(assoc_name))&.klass&.columns&.map(&:name)&.include?(ks.last)
280
+
281
+ # So that we can map an association name to any special alias name used in an AREL query
282
+ ans = (assoc.klass._assoc_names[assoc_name] ||= [])
283
+ ans << assoc.klass unless ans.include?(assoc.klass)
284
+ # There is some potential for duplicates when there is an HM-based where in play. De-duplicate if so.
285
+ has_hm ||= assoc.macro == :has_many
286
+ join_array[assoc_name] = nil # Store this relation name in our special collection for .joins()
122
287
  end
123
288
  wheres[k] = v.split(',')
124
289
  end
125
- unless wheres.empty?
126
- where!(wheres)
127
- joins!(rel_joins) unless rel_joins.empty?
128
- wheres # Return the specific parameters that we did use
290
+ # distinct! if has_hm
291
+
292
+ # %%% Skip the metadata columns
293
+ if selects&.empty? # Default to all columns
294
+ columns.each do |col|
295
+ selects << "#{table.name}.#{col.name}"
296
+ end
129
297
  end
298
+
299
+ # Search for BT, HM, and HMT DSL stuff
300
+ translations = {}
301
+ if is_add_bts || is_add_hms
302
+ bts, hms, associatives = ::Brick.get_bts_and_hms(klass)
303
+ bts.each do |_k, bt|
304
+ # join_array[bt.first] = nil # Store this relation name in our special collection for .joins()
305
+ bt_descrip[bt.first] = [bt.last, bt.last.brick_parse_dsl(join_array, bt.first, translations)]
306
+ end
307
+ skip_klass_hms = ::Brick.config.skip_index_hms[klass.name] || {}
308
+ hms.each do |k, hm|
309
+ next if skip_klass_hms.key?(k)
310
+
311
+ join_array[k] = nil # Store this relation name in our special collection for .joins()
312
+ hm_counts[k] = nil # Placeholder that will be filled in once we know the proper table alias
313
+ end
314
+ end
315
+ where!(wheres) unless wheres.empty?
316
+ if join_array.present?
317
+ left_outer_joins!(join_array) # joins!(join_array)
318
+ # Without working from a duplicate, touching the AREL ast tree sets the @arel instance variable, which causes the relation to be immutable.
319
+ (rel_dupe = dup)._arel_alias_names
320
+ core_selects = selects.dup
321
+ groups = []
322
+ chains = rel_dupe._brick_chains
323
+ id_for_tables = {}
324
+ bt_columns = bt_descrip.each_with_object([]) do |v, s|
325
+ tbl_name = chains[v.last.first].first
326
+ if (id_col = v.last.first.primary_key) && !id_for_tables.key?(tbl_name)
327
+ groups << (unaliased = "#{tbl_name}.#{id_col}")
328
+ selects << "#{unaliased} AS \"#{(id_alias = id_for_tables[tbl_name] = "_brfk_#{v.first}__#{id_col}")}\""
329
+ v.last << id_alias
330
+ end
331
+ if (col_name = v.last[1].last&.last)
332
+ v.last[1].map { |x| [translations[x[0..-2].map(&:to_s).join('.')], x.last] }.each_with_index do |sel_col, idx|
333
+ groups << (unaliased = "#{tbl_name = chains[sel_col.first].first}.#{sel_col.last}")
334
+ # col_name is weak when there are multiple, using sel_col.last instead
335
+ tbl_name2 = tbl_name.start_with?('public.') ? tbl_name[7..-1] : tbl_name
336
+ selects << "#{unaliased} AS \"#{(col_alias = "_brfk_#{tbl_name2}__#{sel_col.last}")}\""
337
+ v.last[1][idx] << col_alias
338
+ end
339
+ end
340
+ end
341
+ group!(core_selects + groups) if hm_counts.any? # + bt_columns
342
+ join_array.each do |assoc_name|
343
+ # %%% Need to support {user: :profile}
344
+ next unless assoc_name.is_a?(Symbol)
345
+
346
+ klass = reflect_on_association(assoc_name)&.klass
347
+ table_alias = chains[klass].length > 1 ? chains[klass].shift : chains[klass].first
348
+ _assoc_names[assoc_name] = [table_alias, klass]
349
+ end
350
+ # Copy entries over
351
+ hm_counts.keys.each do |k|
352
+ hm_counts[k] = _assoc_names[k]
353
+ end
354
+ end
355
+ wheres unless wheres.empty? # Return the specific parameters that we did use
130
356
  end
131
357
  end
132
358
 
133
359
  module Inheritance
134
360
  module ClassMethods
135
- private
361
+ private
136
362
 
137
363
  alias _brick_find_sti_class find_sti_class
138
364
  def find_sti_class(type_name)
139
365
  if ::Brick.sti_models.key?(type_name)
140
- # puts ['X', self.name, type_name].inspect
141
366
  _brick_find_sti_class(type_name)
142
367
  else
143
368
  # This auto-STI is more of a brute-force approach, building modules where needed
@@ -146,10 +371,8 @@ module ActiveRecord
146
371
  module_prefixes = type_name.split('::')
147
372
  module_prefixes.unshift('') unless module_prefixes.first.blank?
148
373
  module_name = module_prefixes[0..-2].join('::')
149
- if ::Brick.config.sti_namespace_prefixes&.key?("::#{module_name}::") ||
150
- ::Brick.config.sti_namespace_prefixes&.key?("#{module_name}::")
151
- _brick_find_sti_class(type_name)
152
- elsif File.exists?(candidate_file = Rails.root.join('app/models' + module_prefixes.map(&:underscore).join('/') + '.rb'))
374
+ if (snp = ::Brick.config.sti_namespace_prefixes)&.key?("::#{module_name}::") || snp&.key?("#{module_name}::") ||
375
+ File.exist?(candidate_file = Rails.root.join('app/models' + module_prefixes.map(&:underscore).join('/') + '.rb'))
153
376
  _brick_find_sti_class(type_name) # Find this STI class normally
154
377
  else
155
378
  # Build missing prefix modules if they don't yet exist
@@ -228,10 +451,9 @@ class Object
228
451
  end
229
452
 
230
453
  relations = ::Brick.instance_variable_get(:@relations)[ActiveRecord::Base.connection_pool.object_id] || {}
231
- is_controllers_enabled = ::Brick.enable_controllers? || (ENV['RAILS_ENV'] || ENV['RACK_ENV']) == 'development'
232
- result = if is_controllers_enabled && class_name.end_with?('Controller') && (plural_class_name = class_name[0..-11]).length.positive?
454
+ result = if ::Brick.enable_controllers? && class_name.end_with?('Controller') && (plural_class_name = class_name[0..-11]).length.positive?
233
455
  # Otherwise now it's up to us to fill in the gaps
234
- if (model = ActiveSupport::Inflector.singularize(plural_class_name).constantize)
456
+ if (model = plural_class_name.singularize.constantize)
235
457
  # 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.
236
458
  build_controller(class_name, plural_class_name, model, relations)
237
459
  end
@@ -243,10 +465,10 @@ class Object
243
465
 
244
466
  # Adjust for STI if we know of a base model for the requested model name
245
467
  table_name = if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, nil))
246
- base_model.table_name
247
- else
248
- ActiveSupport::Inflector.pluralize(singular_table_name)
249
- end
468
+ base_model.table_name
469
+ else
470
+ ActiveSupport::Inflector.pluralize(singular_table_name)
471
+ end
250
472
 
251
473
  # Maybe, just maybe there's a database table that will satisfy this need
252
474
  if (matching = [table_name, singular_table_name, plural_class_name, model_name].find { |m| relations.key?(m) })
@@ -282,6 +504,7 @@ class Object
282
504
  end
283
505
  return
284
506
  end
507
+
285
508
  if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, nil))
286
509
  is_sti = true
287
510
  else
@@ -443,13 +666,21 @@ class Object
443
666
 
444
667
  code << " def index\n"
445
668
  code << " @#{table_name} = #{model.name}#{model.primary_key ? ".order(#{model.primary_key.inspect})" : '.all'}\n"
446
- code << " @#{table_name}.brick_where(params)\n"
669
+ code << " @#{table_name}.brick_select(params)\n"
447
670
  code << " end\n"
448
671
  self.define_method :index do
449
672
  ::Brick.set_db_schema(params)
450
- ar_relation = model.primary_key ? model.order(model.primary_key) : model.all
451
- instance_variable_set(:@_brick_params, ar_relation.brick_where(params))
452
- instance_variable_set("@#{table_name}".to_sym, ar_relation)
673
+ ar_relation = model.all # model.primary_key ? model.order(model.primary_key) : model.all
674
+ @_brick_params = ar_relation.brick_select(params, (selects = []), (bt_descrip = {}), (hm_counts = {}), (join_array = ::Brick::JoinArray.new))
675
+ # %%% Add custom HM count columns
676
+ # %%% What happens when the PK is composite?
677
+ counts = hm_counts.each_with_object([]) { |v, s| s << "COUNT(DISTINCT #{v.last.first}.#{v.last.last.primary_key}) AS _br_#{v.first}_ct" }
678
+ # *selects,
679
+ instance_variable_set("@#{table_name}".to_sym, ar_relation.dup._select!(*selects, *counts))
680
+ # binding.pry
681
+ @_brick_bt_descrip = bt_descrip
682
+ @_brick_hm_counts = hm_counts
683
+ @_brick_join_array = join_array
453
684
  end
454
685
 
455
686
  if model.primary_key
@@ -522,9 +753,9 @@ module ActiveRecord::ConnectionHandling
522
753
  end
523
754
 
524
755
  def _brick_reflect_tables
525
- if (relations = ::Brick.relations).empty?
526
- # Only for Postgres? (Doesn't work in sqlite3)
527
- # puts ActiveRecord::Base.connection.execute("SELECT current_setting('SEARCH_PATH')").to_a.inspect
756
+ if (relations = ::Brick.relations).empty?
757
+ # Only for Postgres? (Doesn't work in sqlite3)
758
+ # puts ActiveRecord::Base.connection.execute("SELECT current_setting('SEARCH_PATH')").to_a.inspect
528
759
 
529
760
  schema_sql = 'SELECT NULL AS table_schema;'
530
761
  case ActiveRecord::Base.connection.adapter_name
@@ -614,6 +845,25 @@ module ActiveRecord::ConnectionHandling
614
845
  end
615
846
  end
616
847
 
848
+ # # Add unique OIDs
849
+ # if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
850
+ # ActiveRecord::Base.execute_sql(
851
+ # "SELECT c.oid, n.nspname, c.relname
852
+ # FROM pg_catalog.pg_namespace AS n
853
+ # INNER JOIN pg_catalog.pg_class AS c ON n.oid = c.relnamespace
854
+ # WHERE c.relkind IN ('r', 'v')"
855
+ # ).each do |r|
856
+ # next if ['pg_catalog', 'information_schema', ''].include?(r['nspname']) ||
857
+ # ['ar_internal_metadata', 'schema_migrations'].include?(r['relname'])
858
+ # relation = relations.fetch(r['relname'], nil)
859
+ # if relation
860
+ # (relation[:oid] ||= {})[r['nspname']] = r['oid']
861
+ # else
862
+ # puts "Where is #{r['nspname']} #{r['relname']} ?"
863
+ # end
864
+ # end
865
+ # end
866
+
617
867
  case ActiveRecord::Base.connection.adapter_name
618
868
  when 'PostgreSQL', 'Mysql2'
619
869
  sql = ActiveRecord::Base.send(:sanitize_sql_array, [
@@ -706,7 +956,7 @@ module Brick
706
956
  missing << fk[0] unless relations.key?(fk[0])
707
957
  missing << primary_table unless is_class || relations.key?(primary_table)
708
958
  unless missing.empty?
709
- tables = relations.reject { |k, v| v.fetch(:isView, nil) }.keys.sort
959
+ tables = relations.reject { |_k, v| v.fetch(:isView, nil) }.keys.sort
710
960
  puts "Brick: Additional reference #{fk.inspect} refers to non-existent #{'table'.pluralize(missing.length)} #{missing.join(' and ')}. (Available tables include #{tables.join(', ')}.)"
711
961
  return
712
962
  end
@@ -715,7 +965,7 @@ module Brick
715
965
  puts "Brick: Additional reference #{fk.inspect} refers to non-existent column #{fk[1]}. (Columns present in #{fk[0]} are #{columns.join(', ')}.)"
716
966
  return
717
967
  end
718
- if (redundant = bts.find { |k, v| v[:inverse]&.fetch(:inverse_table, nil) == fk[0] && v[:fk] == fk[1] && v[:inverse_table] == primary_table })
968
+ if (redundant = bts.find { |_k, v| v[:inverse]&.fetch(:inverse_table, nil) == fk[0] && v[:fk] == fk[1] && v[:inverse_table] == primary_table })
719
969
  if is_class && !redundant.last.key?(:class)
720
970
  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
721
971
  else
@@ -737,19 +987,19 @@ module Brick
737
987
  # assoc_bt[:inverse_of] = primary_class.reflect_on_all_associations.find { |a| a.foreign_key == bt[1] }
738
988
  end
739
989
 
740
- unless is_class || ::Brick.config.exclude_hms&.any? { |exclusion| fk[0] == exclusion[0] && fk[1] == exclusion[1] && primary_table == exclusion[2] }
741
- cnstr_name = "hm_#{cnstr_name}"
742
- if (assoc_hm = hms.fetch(cnstr_name, nil))
743
- assoc_hm[:fk] = assoc_hm[:fk].is_a?(String) ? [assoc_hm[:fk], fk[1]] : assoc_hm[:fk].concat(fk[1])
744
- assoc_hm[:alternate_name] = "#{assoc_hm[:alternate_name]}_#{bt_assoc_name}" unless assoc_hm[:alternate_name] == bt_assoc_name
745
- assoc_hm[:inverse] = assoc_bt
746
- else
747
- 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 }
748
- hm_counts = relation.fetch(:hm_counts) { relation[:hm_counts] = {} }
749
- hm_counts[fk[0]] = hm_counts.fetch(fk[0]) { 0 } + 1
750
- end
751
- assoc_bt[:inverse] = assoc_hm
990
+ return if is_class || ::Brick.config.exclude_hms&.any? { |exclusion| fk[0] == exclusion[0] && fk[1] == exclusion[1] && primary_table == exclusion[2] }
991
+
992
+ cnstr_name = "hm_#{cnstr_name}"
993
+ if (assoc_hm = hms.fetch(cnstr_name, nil))
994
+ assoc_hm[:fk] = assoc_hm[:fk].is_a?(String) ? [assoc_hm[:fk], fk[1]] : assoc_hm[:fk].concat(fk[1])
995
+ assoc_hm[:alternate_name] = "#{assoc_hm[:alternate_name]}_#{bt_assoc_name}" unless assoc_hm[:alternate_name] == bt_assoc_name
996
+ assoc_hm[:inverse] = assoc_bt
997
+ else
998
+ 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 }
999
+ hm_counts = relation.fetch(:hm_counts) { relation[:hm_counts] = {} }
1000
+ hm_counts[fk[0]] = hm_counts.fetch(fk[0]) { 0 } + 1
752
1001
  end
1002
+ assoc_bt[:inverse] = assoc_hm
753
1003
  # hms[cnstr_name] << { is_bt: false, fk: fk[1], assoc_name: fk[0], alternate_name: bt_assoc_name, inverse_table: fk[0] }
754
1004
  end
755
1005
  end
@@ -7,10 +7,12 @@ module Brick
7
7
  # paths['app/models'] << 'lib/brick/frameworks/active_record/models'
8
8
  config.brick = ActiveSupport::OrderedOptions.new
9
9
  ActiveSupport.on_load(:before_initialize) do |app|
10
+ is_development = (ENV['RAILS_ENV'] || ENV['RACK_ENV']) == 'development'
10
11
  ::Brick.enable_models = app.config.brick.fetch(:enable_models, 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)
12
+ ::Brick.enable_controllers = app.config.brick.fetch(:enable_controllers, is_development)
13
+ require 'brick/join_array' if ::Brick.enable_controllers?
14
+ ::Brick.enable_views = app.config.brick.fetch(:enable_views, is_development)
15
+ ::Brick.enable_routes = app.config.brick.fetch(:enable_routes, is_development)
14
16
  ::Brick.skip_database_views = app.config.brick.fetch(:skip_database_views, false)
15
17
 
16
18
  # Specific database tables and views to omit when auto-creating models
@@ -43,7 +45,7 @@ module Brick
43
45
  # ====================================
44
46
  # Dynamically create generic templates
45
47
  # ====================================
46
- if ::Brick.enable_views? || (ENV['RAILS_ENV'] || ENV['RACK_ENV']) == 'development'
48
+ if ::Brick.enable_views?
47
49
  ActionView::LookupContext.class_exec do
48
50
  alias :_brick_template_exists? :template_exists?
49
51
  def template_exists?(*args, **options)
@@ -51,14 +53,12 @@ module Brick
51
53
  # Need to return true if we can fill in the blanks for a missing one
52
54
  # args will be something like: ["index", ["categories"]]
53
55
  model = args[1].map(&:camelize).join('::').singularize.constantize
54
- if (
55
- is_template_exists = model && (
56
- ['index', 'show'].include?(args.first) || # Everything has index and show
57
- # Only CRU stuff has create / update / destroy
58
- (!model.is_view? && ['new', 'create', 'edit', 'update', 'destroy'].include?(args.first))
59
- )
60
- )
61
- instance_variable_set(:@_brick_model, model)
56
+ if is_template_exists = model && (
57
+ ['index', 'show'].include?(args.first) || # Everything has index and show
58
+ # Only CUD stuff has create / update / destroy
59
+ (!model.is_view? && ['new', 'create', 'edit', 'update', 'destroy'].include?(args.first))
60
+ )
61
+ @_brick_model = model
62
62
  end
63
63
  end
64
64
  is_template_exists
@@ -66,56 +66,50 @@ module Brick
66
66
 
67
67
  alias :_brick_find_template :find_template
68
68
  def find_template(*args, **options)
69
- if @_brick_model
70
- model_name = @_brick_model.name
71
- pk = @_brick_model.primary_key
72
- obj_name = model_name.underscore
73
- table_name = model_name.pluralize.underscore
74
- # This gets has_many as well as has_many :through
75
- # %%% weed out ones that don't have an available model to reference
76
- bts, hms = ::Brick.get_bts_and_hms(@_brick_model)
77
- # Mark has_manys that go to an associative ("join") table so that they are skipped in the UI,
78
- # as well as any possible polymorphic associations
79
- exclude_hms = {}
80
- associatives = hms.each_with_object({}) do |hmt, s|
81
- if (through = hmt.last.options[:through])
82
- exclude_hms[through] = nil
83
- s[hmt.first] = hms[through] # End up with a hash of HMT names pointing to join-table associations
84
- elsif hmt.last.inverse_of.nil?
85
- puts "SKIPPING #{hmt.last.name.inspect}"
86
- # %%% If we don't do this then below associative.name will find that associative is nil
87
- exclude_hms[hmt.last.name] = nil
88
- end
89
- end
69
+ return _brick_find_template(*args, **options) unless @_brick_model
70
+
71
+ model_name = @_brick_model.name
72
+ pk = @_brick_model.primary_key
73
+ obj_name = model_name.underscore
74
+ table_name = model_name.pluralize.underscore
75
+ bts, hms, associatives = ::Brick.get_bts_and_hms(@_brick_model) # This gets BT and HM and also has_many :through (HMT)
76
+ hms_columns = +'' # Used for 'index'
77
+ skip_klass_hms = ::Brick.config.skip_index_hms[model_name] || {}
78
+ hms_headers = hms.each_with_object([]) do |hm, s|
79
+ hm_assoc = hm.last
80
+ if args.first == 'index'
81
+ hm_fk_name = if hm_assoc.options[:through]
82
+ associative = associatives[hm_assoc.name]
83
+ "'#{associative.name}.#{associative.foreign_key}'"
84
+ else
85
+ hm_assoc.foreign_key
86
+ end
87
+ hms_columns << if hm_assoc.macro == :has_many
88
+ set_ct = if skip_klass_hms.key?((assoc_name = hm.first).to_sym)
89
+ 'nil'
90
+ else
91
+ "#{obj_name}._br_#{assoc_name}_ct"
92
+ end
90
93
 
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 - ::Brick.config.exclude_tables)
93
- .each_with_object(+'') { |v, s| s << "<option value=\"#{v}\">#{v}</option>" }.html_safe
94
- hms_columns = +'' # Used for 'index'
95
- hms_headers = hms.each_with_object([]) do |hm, s|
96
- next if exclude_hms.key?((hm_assoc = hm.last).name)
97
-
98
- if args.first == 'index'
99
- hm_fk_name = if hm_assoc.options[:through]
100
- associative = associatives[hm_assoc.name]
101
- "'#{associative.name}.#{associative.foreign_key}'"
102
- else
103
- hm_assoc.foreign_key
104
- end
105
- hms_columns << if hm_assoc.macro == :has_many
106
94
  "<td>
107
- <%= 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? %>
95
+ <%= ct = #{set_ct}
96
+ link_to \"#\{ct || 'View'\} #{assoc_name}\", #{hm_assoc.klass.name.underscore.pluralize}_path({ #{hm_fk_name}: #{obj_name}.#{pk} }) unless ct&.zero? %>
108
97
  </td>\n"
109
- else # has_one
98
+ else # has_one
110
99
  "<td>
111
100
  <%= obj = #{obj_name}.#{hm.first}; link_to(obj.brick_descrip, obj) if obj %>
112
101
  </td>\n"
113
- end
114
- end
115
- s << [hm_assoc, "H#{hm_assoc.macro == :has_one ? 'O' : 'M'}#{'T' if hm_assoc.options[:through]} #{hm.first}"]
102
+ end
116
103
  end
104
+ s << [hm_assoc, "H#{hm_assoc.macro == :has_one ? 'O' : 'M'}#{'T' if hm_assoc.options[:through]} #{hm.first}"]
105
+ end
117
106
 
118
- css = "<style>
107
+ schema_options = ::Brick.db_schemas.each_with_object(+'') { |v, s| s << "<option value=\"#{v}\">#{v}</option>" }.html_safe
108
+ # %%% If we are not auto-creating controllers (or routes) then omit by default, and if enabled anyway, such as in a development
109
+ # environment or whatever, then get either the controllers or routes list instead
110
+ table_options = (::Brick.relations.keys - ::Brick.config.exclude_tables)
111
+ .each_with_object(+'') { |v, s| s << "<option value=\"#{v.underscore.pluralize}\">#{v}</option>" }.html_safe
112
+ css = +"<style>
119
113
  table {
120
114
  border-collapse: collapse;
121
115
  margin: 25px 0;
@@ -189,7 +183,12 @@ def hide_bcrypt(val)
189
183
  is_bcrypt?(val) ? '(hidden)' : val
190
184
  end %>"
191
185
 
192
- script = "<script>
186
+ if ['index', 'show', 'update'].include?(args.first)
187
+ css << "<% 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(', ')} } %>"
188
+ end
189
+
190
+ # %%% When doing schema select, if there's an ID then remove it, or if we're on a new page go to index
191
+ script = "<script>
193
192
  var schemaSelect = document.getElementById(\"schema\");
194
193
  var brickSchema;
195
194
  if (schemaSelect) {
@@ -243,9 +242,8 @@ function changeout(href, param, value) {
243
242
  return hrefParts[0] + \"?\" + Object.keys(params).reduce(function (s, v) { s.push(v + \"=\" + params[v]); return s; }, []).join(\"&\");
244
243
  }
245
244
  </script>"
246
-
247
- inline = case args.first
248
- when 'index'
245
+ inline = case args.first
246
+ when 'index'
249
247
  "#{css}
250
248
  <p style=\"color: green\"><%= notice %></p>#{"
251
249
  <select id=\"schema\">#{schema_options}</select>" if ::Brick.db_schemas.length > 1}
@@ -253,9 +251,8 @@ function changeout(href, param, value) {
253
251
  <h1>#{model_name.pluralize}</h1>
254
252
  <% if @_brick_params&.present? %><h3>where <%= @_brick_params.each_with_object([]) { |v, s| s << \"#\{v.first\} = #\{v.last.inspect\}\" }.join(', ') %></h3><% end %>
255
253
  <table id=\"#{table_name}\">
256
- <thead><tr>#{"<th></th>" if pk }
257
- <% 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(', ')} }
258
- @#{table_name}.columns.map(&:name).each do |col| %>
254
+ <thead><tr>#{'<th></th>' if pk}
255
+ <% @#{table_name}.columns.map(&:name).each do |col| %>
259
256
  <% next if col == '#{pk}' || ::Brick.config.metadata_columns.include?(col) %>
260
257
  <th>
261
258
  <% if (bt = bts[col]) %>
@@ -271,16 +268,16 @@ function changeout(href, param, value) {
271
268
  <tbody>
272
269
  <% @#{table_name}.each do |#{obj_name}| %>
273
270
  <tr>#{"
274
- <td><%= link_to '⇛', #{obj_name}_path(#{obj_name}.#{pk}), { class: 'big-arrow' } %></td>" if pk }
271
+ <td><%= link_to '⇛', #{obj_name}_path(#{obj_name}.#{pk}), { class: 'big-arrow' } %></td>" if pk}
275
272
  <% #{obj_name}.attributes.each do |k, val| %>
276
- <% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) %>
273
+ <% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) || k.start_with?('_brfk_') || (k.start_with?('_br_') && k.end_with?('_ct')) %>
277
274
  <td>
278
275
  <% if (bt = bts[k]) %>
279
- <%# Instead of just 'bt_obj we have to put in all of this junk:
280
- # send(\"#\{bt_obj_class = bt[1].name.underscore\}_path\".to_sym, bt_obj.send(bt[1].primary_key.to_sym))
281
- # Otherwise we get stuff like:
282
- # ActionView::Template::Error (undefined method `vehicle_path' for #<ActionView::Base:0x0000000033a888>) %>
283
- <%= 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 %>
276
+ <%# binding.pry if bt.first == :user %>
277
+ <% bt_txt = bt[1].brick_descrip(#{obj_name}, @_brick_bt_descrip[bt.first][1].map { |z| #{obj_name}.send(z.last) }, @_brick_bt_descrip[bt.first][2]) %>
278
+ <% bt_id_col = @_brick_bt_descrip[bt.first][2]; bt_id = #{obj_name}.send(bt_id_col) if bt_id_col %>
279
+ <%= bt_id ? link_to(bt_txt, send(\"#\{bt_obj_path_base = bt[1].name.underscore\}_path\".to_sym, bt_id)) : bt_txt %>
280
+ <%#= Previously was: bt_obj = bt[1].find_by(bt[2] => val); link_to(bt_obj.brick_descrip, send(\"#\{bt_obj_path_base = bt[1].name.underscore\}_path\".to_sym, bt_obj.send(bt[1].primary_key.to_sym))) if bt_obj %>
284
281
  <% else %>
285
282
  <%= hide_bcrypt(val) %>
286
283
  <% end %>
@@ -295,7 +292,7 @@ function changeout(href, param, value) {
295
292
 
296
293
  #{"<hr><%= link_to \"New #{obj_name}\", new_#{obj_name}_path %>" unless @_brick_model.is_view?}
297
294
  #{script}"
298
- when 'show', 'update'
295
+ when 'show', 'update'
299
296
  "#{css}
300
297
  <p style=\"color: green\"><%= notice %></p>#{"
301
298
  <select id=\"schema\">#{schema_options}</select>" if ::Brick.db_schemas.length > 1}
@@ -308,8 +305,7 @@ function changeout(href, param, value) {
308
305
  # url = send(:#{model_name.underscore}_path, obj.#{pk})
309
306
  form_for(obj) do |f| %>
310
307
  <table>
311
- <% 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(', ')} }
312
- @#{obj_name}.first.attributes.each do |k, val| %>
308
+ <% @#{obj_name}.first.attributes.each do |k, val| %>
313
309
  <tr>
314
310
  <% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) %>
315
311
  <th class=\"show-field\">
@@ -331,7 +327,7 @@ function changeout(href, param, value) {
331
327
  html_options = { prompt: \"Select #\{bt_name\}\" }
332
328
  html_options[:class] = 'dimmed' unless val %>
333
329
  <%= f.select k.to_sym, bt[3], { value: val || '^^^brick_NULL^^^' }, html_options %>
334
- <%= bt_obj = bt[1].find_by(bt[2] => val); link_to('⇛', send(\"#\{bt_obj_class = bt_name.underscore\}_path\".to_sym, bt_obj.send(bt[1].primary_key.to_sym)), { class: 'show-arrow' }) if bt_obj %>
330
+ <%= bt_obj = bt[1].find_by(bt[2] => val); link_to('⇛', send(\"#\{bt_obj_path_base = bt_name.underscore\}_path\".to_sym, bt_obj.send(bt[1].primary_key.to_sym)), { class: 'show-arrow' }) if bt_obj %>
335
331
  <% else case #{model_name}.column_for_attribute(k).type
336
332
  when :string, :text %>
337
333
  <% if is_bcrypt?(val) # || .readonly? %>
@@ -341,8 +337,13 @@ function changeout(href, param, value) {
341
337
  <% end %>
342
338
  <% when :boolean %>
343
339
  <%= f.check_box k.to_sym %>
344
- <% when :integer, :date, :datetime, :decimal %>
340
+ <% when :integer, :decimal, :float, :date, :datetime, :time, :timestamp
341
+ # What happens when keys are UUID?
342
+ # Postgres naturally uses the +uuid_generate_v4()+ function from the uuid-ossp extension
343
+ # If it's not yet enabled then: enable_extension 'uuid-ossp'
344
+ # ActiveUUID gem created a new :uuid type %>
345
345
  <%= val %>
346
+ <% when :binary, :primary_key %>
346
347
  <% end %>
347
348
  <% end %>
348
349
  </td>
@@ -354,6 +355,7 @@ function changeout(href, param, value) {
354
355
 
355
356
  #{hms_headers.map do |hm|
356
357
  next unless (pk = hm.first.klass.primary_key)
358
+
357
359
  "<table id=\"#{hm_name = hm.first.name.to_s}\">
358
360
  <tr><th>#{hm.last}</th></tr>
359
361
  <% collection = @#{obj_name}.first.#{hm_name}
@@ -369,19 +371,16 @@ function changeout(href, param, value) {
369
371
  <% end %>
370
372
  #{script}"
371
373
 
372
- end
373
- # As if it were an inline template (see #determine_template in actionview-5.2.6.2/lib/action_view/renderer/template_renderer.rb)
374
- keys = options.has_key?(:locals) ? options[:locals].keys : []
375
- handler = ActionView::Template.handler_for_extension(options[:type] || 'erb')
376
- ActionView::Template.new(inline, "auto-generated #{args.first} template", handler, locals: keys)
377
- else
378
- _brick_find_template(*args, **options)
379
- end
374
+ end
375
+ # As if it were an inline template (see #determine_template in actionview-5.2.6.2/lib/action_view/renderer/template_renderer.rb)
376
+ keys = options.has_key?(:locals) ? options[:locals].keys : []
377
+ handler = ActionView::Template.handler_for_extension(options[:type] || 'erb')
378
+ ActionView::Template.new(inline, "auto-generated #{args.first} template", handler, locals: keys)
380
379
  end
381
380
  end
382
381
  end
383
382
 
384
- if ::Brick.enable_routes? || (ENV['RAILS_ENV'] || ENV['RACK_ENV']) == 'development'
383
+ if ::Brick.enable_routes?
385
384
  ActionDispatch::Routing::RouteSet.class_exec do
386
385
  # In order to defer auto-creation of any routes that already exist, calculate Brick routes only after having loaded all others
387
386
  prepend ::Brick::RouteSet
@@ -391,17 +390,6 @@ function changeout(href, param, value) {
391
390
  # Just in case it hadn't been done previously when we tried to load the brick initialiser,
392
391
  # go make sure we've loaded additional references (virtual foreign keys).
393
392
  ::Brick.load_additional_references
394
-
395
- # Find associative tables that can be set up for has_many :through
396
- ::Brick.relations.each do |_key, tbl|
397
- tbl_cols = tbl[:cols].keys
398
- fks = tbl[:fks].each_with_object({}) { |fk, s| s[fk.last[:fk]] = [fk.last[:assoc_name], fk.last[:inverse_table]] if fk.last[:is_bt]; s }
399
- # Aside from the primary key and the metadata columns created_at, updated_at, and deleted_at, if this table only has
400
- # foreign keys then it can act as an associative table and thus be used with has_many :through.
401
- if fks.length > 1 && (tbl_cols - fks.keys - (::Brick.config.metadata_columns || []) - (tbl[:pkey].values.first || [])).length.zero?
402
- fks.each { |fk| tbl[:hmt_fks][fk.first] = fk.last }
403
- end
404
- end
405
393
  end
406
394
  end
407
395
  end
@@ -0,0 +1,227 @@
1
+ module Brick
2
+ # JoinArray and JoinHash
3
+ #
4
+ # These JOIN-related collection classes -- JoinArray and its related "partner in crime" JoinHash -- both interact to
5
+ # more easily build out nested sets of hashes and arrays to be used with ActiveRecord's .joins() method. For example,
6
+ # if there is an Order, Customer, and Employee model, and Order belongs_to :customer and :employee, then from the
7
+ # perspective of Order all these three could be JOINed together by referencing the two belongs_to association names:
8
+ #
9
+ # Order.joins([:customer, :employee])
10
+ #
11
+ # and from the perspective of Employee it would instead use a hash like this, using the has_many :orders association
12
+ # and the :customer belongs_to:
13
+ #
14
+ # Employee.joins({ orders: :customer })
15
+ #
16
+ # (in both cases the same three tables are being JOINed, the two approaches differ just based on their starting standpoint.)
17
+ # These utility classes are designed to make building out any goofy linkages like this pretty simple in a few ways:
18
+ # ** if the same association is requested more than once then no duplicates.
19
+ # ** If a bunch of intermediary associations are referenced leading up to a final one then all of them get automatically built
20
+ # out and added along the way, without any having to previously exist.
21
+ # ** If one reference was made previously and now another neighbouring one is called for, then what used to be a simple symbol
22
+ # is automatically graduated into an array so that both members can be held. For instance, if with the Order example above
23
+ # there was also a LineItem model that belongs_to Order, then let's say you start from LineItem and want to now get all 4
24
+ # related models. You could start by going through :order to :employee like this:
25
+ #
26
+ # line_item_joins = JoinArray.new
27
+ # line_item_joins[:order] = :employee
28
+ # => { order: :employee }
29
+ #
30
+ # and then add in the reference to :customer like this:
31
+ #
32
+ # line_item_joins[:order] = :customer
33
+ # => { order: [:employee, :customer] }
34
+ #
35
+ # and then carry on incrementally building out more JOINs in whatever sequence makes the best sense. This bundle of nested
36
+ # stuff can then be used to query ActiveRecord like this:
37
+ #
38
+ # LineItem.joins(line_item_joins)
39
+
40
+ class JoinArray < Array
41
+ attr_reader :parent, :orig_parent, :parent_key
42
+ alias _brick_set []=
43
+
44
+ def [](*args)
45
+ if !(key = args[0]).is_a?(Symbol)
46
+ super
47
+ else
48
+ idx = -1
49
+ # Whenever a JoinHash has a value of a JoinArray with a single member then it is a wrapper, usually for a Symbol
50
+ matching = find { |x| idx += 1; (x.is_a?(::Brick::JoinArray) && x.first == key) || (x.is_a?(::Brick::JoinHash) && x.key?(key)) || x == key }
51
+ case matching
52
+ when ::Brick::JoinHash
53
+ matching[key]
54
+ when ::Brick::JoinArray
55
+ matching.first
56
+ else
57
+ ::Brick::JoinHash.new.tap do |child|
58
+ child.instance_variable_set(:@parent, self)
59
+ child.instance_variable_set(:@parent_key, key) # %%% Use idx instead of key?
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ def []=(*args)
66
+ ::Brick::JoinArray.attach_back_to_root(self, args[0], args[1])
67
+
68
+ if (key = args[0]).is_a?(Symbol) && ((value = args[1]).is_a?(::Brick::JoinHash) || value.is_a?(Symbol) || value.nil?)
69
+ # %%% This is for the first symbol added to a JoinArray, cleaning out the leftover {} that is temporarily built out
70
+ # when doing my_join_array[:value1][:value2] = nil.
71
+ idx = -1
72
+ delete_at(idx) if value.nil? && any? { |x| idx += 1; x.is_a?(::Brick::JoinHash) && x.empty? }
73
+
74
+ set_matching(key, value)
75
+ else
76
+ super
77
+ end
78
+ end
79
+
80
+ def self.attach_back_to_root(collection, key = nil, value = nil)
81
+ # Create a list of layers which start at the root
82
+ layers = []
83
+ layer = collection
84
+ while layer.parent
85
+ layers << layer
86
+ layer = layer.parent
87
+ end
88
+ # Go through the layers from root down to child, attaching everything
89
+ layers.each do |layer|
90
+ if (prnt = layer.remove_instance_variable(:@parent))
91
+ layer.instance_variable_set(:@orig_parent, prnt)
92
+ end
93
+ case prnt
94
+ when ::Brick::JoinHash
95
+ value = if prnt.key?(layer.parent_key)
96
+ if layer.is_a?(Hash)
97
+ layer
98
+ else
99
+ ::Brick::JoinArray.new.replace([prnt.fetch(layer.parent_key, nil), layer])
100
+ end
101
+ else
102
+ layer
103
+ end
104
+ # This is as if we did: prnt[layer.parent_key] = value
105
+ # but calling it that way would attempt to infinitely recurse back onto this overridden version of the []= method,
106
+ # so we go directly to ._brick_store() instead.
107
+ prnt._brick_store(layer.parent_key, value)
108
+ when ::Brick::JoinArray
109
+ if (key)
110
+ puts "X1"
111
+ prnt[layer.parent_key][key] = value
112
+ else
113
+ prnt[layer.parent_key] = layer
114
+ end
115
+ end
116
+ end
117
+ end
118
+
119
+ def set_matching(key, value)
120
+ idx = -1
121
+ matching = find { |x| idx += 1; (x.is_a?(::Brick::JoinArray) && x.first == key) || (x.is_a?(::Brick::JoinHash) && x.key?(key)) || x == key }
122
+ case matching
123
+ when ::Brick::JoinHash
124
+ matching[key] = value
125
+ when Symbol
126
+ if value.nil? # If it already exists then no worries
127
+ matching
128
+ else
129
+ # Not yet there, so we will "graduate" this single value into being a key / value pair found in a JoinHash. The
130
+ # destination hash to be used will be either an existing one if there is a neighbouring JoinHash available, or a
131
+ # newly-built one placed in the "new_hash" variable if none yet exists.
132
+ hash = find { |x| x.is_a?(::Brick::JoinHash) } || (new_hash = ::Brick::JoinHash.new)
133
+ hash._brick_store(key, ::Brick::JoinArray.new.tap { |val_array| val_array.replace([value]) })
134
+ # hash.instance_variable_set(:@parent, matching.parent) if matching.parent
135
+ # hash.instance_variable_set(:@parent_key, matching.parent_key) if matching.parent_key
136
+
137
+ # When a new JoinHash was created, we place it at the same index where the original lone symbol value was pulled from.
138
+ # If instead we used an existing JoinHash then since that symbol has now been graduated into a new key / value pair in
139
+ # the existing JoinHash then we delete the original symbol by its index.
140
+ new_hash ? _brick_set(idx, new_hash) : delete_at(idx)
141
+ end
142
+ when ::Brick::JoinArray # Replace this single thing (usually a Symbol found as a value in a JoinHash)
143
+ (hash = ::Brick::JoinHash.new)._brick_store(key, value)
144
+ if matching.parent
145
+ hash.instance_variable_set(:@parent, matching.parent)
146
+ hash.instance_variable_set(:@parent_key, matching.parent_key)
147
+ end
148
+ _brick_set(idx, hash)
149
+ else # Doesn't already exist anywhere, so add it to the end of this JoinArray and return the new member
150
+ if value
151
+ ::Brick::JoinHash.new.tap do |hash|
152
+ val_collection = if value.is_a?(::Brick::JoinHash)
153
+ value
154
+ else
155
+ ::Brick::JoinArray.new.tap { |array| array.replace([value]) }
156
+ end
157
+ val_collection.instance_variable_set(:@parent, hash)
158
+ val_collection.instance_variable_set(:@parent_key, key)
159
+ hash._brick_store(key, val_collection)
160
+ hash.instance_variable_set(:@parent, self)
161
+ hash.instance_variable_set(:@parent_key, length)
162
+ end
163
+ else
164
+ key
165
+ end.tap { |member| push(member) }
166
+ end
167
+ end
168
+ end
169
+
170
+ class JoinHash < Hash
171
+ attr_reader :parent, :orig_parent, :parent_key
172
+ alias _brick_store []=
173
+
174
+ def [](*args)
175
+ if (current = super)
176
+ current
177
+ elsif (key = args[0]).is_a?(Symbol)
178
+ ::Brick::JoinHash.new.tap do |child|
179
+ child.instance_variable_set(:@parent, self)
180
+ child.instance_variable_set(:@parent_key, key)
181
+ end
182
+ end
183
+ end
184
+
185
+ def []=(*args)
186
+ ::Brick::JoinArray.attach_back_to_root(self)
187
+
188
+ if !(key = args[0]).is_a?(Symbol) || (!(value = args[1]).is_a?(Symbol) && !value.nil?)
189
+ super # Revert to normal hash behaviour when we're not passed symbols
190
+ else
191
+ case (current = fetch(key, nil))
192
+ when value
193
+ if value.nil? # Setting a single value where nothing yet exists
194
+ case orig_parent
195
+ when ::Brick::JoinHash
196
+ if self.empty? # Convert this empty hash into a JoinArray
197
+ orig_parent._brick_store(parent_key, ::Brick::JoinArray.new.replace([key]))
198
+ else # Call back into []= to use our own logic, this time setting this value from the context of the parent
199
+ orig_parent[parent_key] = key
200
+ end
201
+ when ::Brick::JoinArray
202
+ orig_parent[parent_key][key] = nil
203
+ else # No knowledge of any parent, so all we can do is add this single value right here as { key => nil }
204
+ super
205
+ end
206
+ key
207
+ else # Setting a key / value pair where nothing yet exists
208
+ puts "X2"
209
+ super(key, ::Brick::JoinArray.new.replace([value]))
210
+ value
211
+ end
212
+ when Symbol # Upgrade an existing symbol to be a part of our special JoinArray
213
+ puts "X3"
214
+ super(key, ::Brick::JoinArray.new.replace([current, value]))
215
+ when ::Brick::JoinArray # Concatenate new stuff onto any existing JoinArray
216
+ current.set_matching(value, nil)
217
+ when ::Brick::JoinHash # Graduate an existing hash into being in an array if things are dissimilar
218
+ super(key, ::Brick::JoinArray.new.replace([current, value]))
219
+ value
220
+ else # Perhaps this is part of some hybrid thing
221
+ super(key, ::Brick::JoinArray.new.replace([value]))
222
+ value
223
+ end
224
+ end
225
+ end
226
+ end
227
+ end
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 18
8
+ TINY = 21
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
@@ -102,14 +102,37 @@ module Brick
102
102
  end
103
103
 
104
104
  def get_bts_and_hms(model)
105
- model.reflect_on_all_associations.each_with_object([{}, {}]) do |a, s|
105
+ bts, hms = model.reflect_on_all_associations.each_with_object([{}, {}]) do |a, s|
106
+ next if !const_defined?(a.name.to_s.singularize.camelize) && ::Brick.config.exclude_tables.include?(a.plural_name)
107
+
108
+ # So that we can map an association name to any special alias name used in an AREL query
109
+ ans = (model._assoc_names[a.name] ||= [])
110
+ ans << a.klass unless ans.include?(a.klass)
106
111
  case a.macro
107
112
  when :belongs_to
108
113
  s.first[a.foreign_key] = [a.name, a.klass]
109
- when :has_many, :has_one
114
+ when :has_many, :has_one # This gets has_many as well as has_many :through
115
+ # %%% weed out ones that don't have an available model to reference
110
116
  s.last[a.name] = a
111
117
  end
112
118
  end
119
+ # Mark has_manys that go to an associative ("join") table so that they are skipped in the UI,
120
+ # as well as any possible polymorphic associations
121
+ skip_hms = {}
122
+ associatives = hms.each_with_object({}) do |hmt, s|
123
+ if (through = hmt.last.options[:through])
124
+ skip_hms[through] = nil
125
+ s[hmt.first] = hms[through] # End up with a hash of HMT names pointing to join-table associations
126
+ elsif hmt.last.inverse_of.nil?
127
+ puts "SKIPPING #{hmt.last.name.inspect}"
128
+ # %%% If we don't do this then below associative.name will find that associative is nil
129
+ skip_hms[hmt.last.name] = nil
130
+ end
131
+ end
132
+ skip_hms.each do |k, _v|
133
+ puts hms.delete(k).inspect
134
+ end
135
+ [bts, hms, associatives]
113
136
  end
114
137
 
115
138
  # Switches Brick auto-models on or off, for all threads
@@ -217,6 +240,12 @@ module Brick
217
240
  end
218
241
  end
219
242
 
243
+ # Skip showing counts for these specific has_many associations when building auto-generated #index views
244
+ # @api public
245
+ def skip_index_hms=(value)
246
+ Brick.config.skip_index_hms = value
247
+ end
248
+
220
249
  # Associations to treat as a has_one
221
250
  # @api public
222
251
  def has_ones=(hos)
@@ -254,6 +283,17 @@ module Brick
254
283
  ars.each { |fk| ::Brick._add_bt_and_hm(fk[0..2]) }
255
284
  @_additional_references_loaded = true
256
285
  end
286
+
287
+ # Find associative tables that can be set up for has_many :through
288
+ ::Brick.relations.each do |_key, tbl|
289
+ tbl_cols = tbl[:cols].keys
290
+ fks = tbl[:fks].each_with_object({}) { |fk, s| s[fk.last[:fk]] = [fk.last[:assoc_name], fk.last[:inverse_table]] if fk.last[:is_bt]; s }
291
+ # Aside from the primary key and the metadata columns created_at, updated_at, and deleted_at, if this table only has
292
+ # foreign keys then it can act as an associative table and thus be used with has_many :through.
293
+ if fks.length > 1 && (tbl_cols - fks.keys - (::Brick.config.metadata_columns || []) - (tbl[:pkey].values.first || [])).length.zero?
294
+ fks.each { |fk| tbl[:hmt_fks][fk.first] = fk.last }
295
+ end
296
+ end
257
297
  end
258
298
 
259
299
 
@@ -18,7 +18,7 @@ module Brick
18
18
  desc 'Generates an initializer file for configuring Brick'
19
19
 
20
20
  def create_initializer_file
21
- unless File.exists?(filename = 'config/initializers/brick.rb')
21
+ unless File.exist?(filename = 'config/initializers/brick.rb')
22
22
  # See if we can make suggestions for additional_references
23
23
  resembles_fks = []
24
24
  possible_additional_references = (relations = ::Brick.relations).each_with_object([]) do |v, s|
@@ -47,7 +47,7 @@ module Brick
47
47
  if (relations.fetch(f_table = col_down, nil) ||
48
48
  relations.fetch(f_table = ActiveSupport::Inflector.pluralize(col_down), nil)) &&
49
49
  # Looks pretty promising ... just make sure a model file isn't present
50
- !File.exists?("app/models/#{ActiveSupport::Inflector.singularize(v.first)}.rb")
50
+ !File.exist?("app/models/#{ActiveSupport::Inflector.singularize(v.first)}.rb")
51
51
  s << "['#{v.first}', '#{col}', '#{f_table}']"
52
52
  else
53
53
  resembles_fks << "#{v.first}.#{col}"
@@ -58,24 +58,24 @@ module Brick
58
58
  end
59
59
 
60
60
  bar = case possible_additional_references.length
61
- when 0
61
+ when 0
62
62
  +"# Brick.additional_references = [['orders', 'customer_id', 'customer'],
63
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:
64
+ when 1
65
+ +"# # Here is a possible additional reference that has been auto-identified for the #{ActiveRecord::Base.connection.current_database} database:
66
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:
67
+ else
68
+ +"# # Here are possible additional references that have been auto-identified for the #{ActiveRecord::Base.connection.current_database} database:
69
69
  # Brick.additional_references = [
70
70
  # #{possible_additional_references.join(",\n# ")}
71
71
  # ]"
72
- end
72
+ end
73
73
  if resembles_fks.length > 0
74
74
  bar << "\n# # Columns named somewhat like a foreign key which you may want to consider:
75
75
  # # #{resembles_fks.join(', ')}"
76
76
  end
77
77
 
78
- create_file filename, "# frozen_string_literal: true
78
+ create_file(filename, "# frozen_string_literal: true
79
79
 
80
80
  # # Settings for the Brick gem
81
81
  # # (By default this auto-creates models, controllers, views, and routes on-the-fly.)
@@ -111,11 +111,16 @@ module Brick
111
111
  # # to be the primary key.)
112
112
  #{bar}
113
113
 
114
- # # Skip creating a has_many association for these
114
+ # # Skip creating a has_many association for these (only retain the belongs_to built from this additional_reference).
115
115
  # # (Uses the same exact three-part format as would define an additional_reference)
116
116
  # # Say for instance that we didn't care to display the favourite colours that users have:
117
117
  # Brick.exclude_hms = [['users', 'favourite_colour_id', 'colours']]
118
118
 
119
+ # # Skip showing counts for these specific has_many associations when building auto-generated #index views.
120
+ # # When there are related tables with a significant number of records, this can lessen the load on the database
121
+ # # 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']
123
+
119
124
  # # By default primary tables involved in a foreign key relationship will indicate a \"has_many\" relationship pointing
120
125
  # # back to the foreign table. In order to represent a \"has_one\" association instead, an override can be provided
121
126
  # # using the primary model name and the association name which you instead want to have treated as a \"has_one\":
@@ -159,7 +164,7 @@ module Brick
159
164
  # Brick.default_route_fallback = 'customers' # This defaults to \"customers/index\"
160
165
  # Brick.default_route_fallback = 'orders/outstanding' # Example of a non-RESTful route
161
166
  # Brick.default_route_fallback = '' # Omits setting a default route in the absence of any other
162
- "
167
+ ")
163
168
  end
164
169
  end
165
170
 
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.18
4
+ version: 1.0.21
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-27 00:00:00.000000000 Z
11
+ date: 2022-05-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -221,6 +221,7 @@ files:
221
221
  - lib/brick/frameworks/rails/controller.rb
222
222
  - lib/brick/frameworks/rails/engine.rb
223
223
  - lib/brick/frameworks/rspec.rb
224
+ - lib/brick/join_array.rb
224
225
  - lib/brick/serializers/json.rb
225
226
  - lib/brick/serializers/yaml.rb
226
227
  - lib/brick/util.rb