brick 1.0.19 → 1.0.22

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: 57a1d4e3774c90984cfc435d9ed87e42b74c364b01fd7ced7df740f5005b935e
4
- data.tar.gz: 8222088ef88ca48cd90144c8503638644195f2d593ff95d7b2e3530304ac6fb2
3
+ metadata.gz: 82ebef3561b9cd7b8abcbbd8a30b7687485543ddf1a0900a9935dfb99299f13f
4
+ data.tar.gz: ba071d53409fd8d4833693907364c745585aeb0bb18e885fcf7cbf1bd23b9940
5
5
  SHA512:
6
- metadata.gz: 2b785c34e1a21563232ae54f6af25989fe02b73f92eb1327b27d6f21e80b7e353a5995305c4c1188dda885465a196f87fb5979a13c3e6a0219d4895dc59593dd
7
- data.tar.gz: bd2018d48dad15f51d7a87e39d2710bde9a5becbe969e741004bf730f57faecbd84a81a2f75cfdd60c2ff48f239fe6c5c8d3f47b4b2f79e355b7f100f9bb7943
6
+ metadata.gz: 69ad3ae158a8e478864db65dc808152d4b1b16bdc8779b1a651f59f199167daf00d2aa10018c5f65d5e619bbca4c09e99bc319ca736eed57023d61e6067ea365
7
+ data.tar.gz: 18ca63e176e12ea50a5168e835a2eddfa66c8f522943d42e6d895d2f3e05d0cf6d396ab28d79f98898c91670b618cc27dbad5fa0bc9129a3edcfc3f74f5db029
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,13 +165,25 @@ 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 = pk_alias.map { |pk_alias_part| obj.send(pk_alias_part) })
170
+ "#{klass.name} ##{id.join(', ')}"
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
 
179
+ def self.bt_link(assoc_name)
180
+ model_underscore = name.underscore
181
+ assoc_name = CGI.escapeHTML(assoc_name.to_s)
182
+ model_path = Rails.application.routes.url_helpers.send("#{model_underscore.pluralize}_path".to_sym)
183
+ link = Class.new.extend(ActionView::Helpers::UrlHelper).link_to(name, model_path)
184
+ model_underscore == assoc_name ? link : "#{assoc_name}-#{link}".html_safe
185
+ end
186
+
101
187
  private
102
188
 
103
189
  def self._brick_get_fks
@@ -106,9 +192,114 @@ module ActiveRecord
106
192
  end
107
193
 
108
194
  class Relation
109
- def brick_where(params)
195
+ attr_reader :_brick_chains
196
+
197
+ # CLASS STUFF
198
+ def _recurse_arel(piece, prefix = '')
199
+ names = []
200
+ # Our JOINs mashup of nested arrays and hashes
201
+ # binding.pry if defined?(@arel)
202
+ case piece
203
+ when Array
204
+ names += piece.inject([]) { |s, v| s + _recurse_arel(v, prefix) }
205
+ when Hash
206
+ names += piece.inject([]) do |s, v|
207
+ new_prefix = "#{prefix}#{v.first}_"
208
+ s << [v.last.shift, new_prefix]
209
+ s + _recurse_arel(v.last, new_prefix)
210
+ end
211
+
212
+ # ActiveRecord AREL objects
213
+ when Arel::Nodes::Join # INNER or OUTER JOIN
214
+ # rubocop:disable Style/IdenticalConditionalBranches
215
+ if piece.right.is_a?(Arel::Table) # Came in from AR < 3.2?
216
+ # Arel 2.x and older is a little curious because these JOINs work "back to front".
217
+ # The left side here is either another earlier JOIN, or at the end of the whole tree, it is
218
+ # the first table.
219
+ names += _recurse_arel(piece.left)
220
+ # The right side here at the top is the very last table, and anywhere else down the tree it is
221
+ # the later "JOIN" table of this pair. (The table that comes after all the rest of the JOINs
222
+ # from the left side.)
223
+ names << [piece.right._arel_table_type, (piece.right.table_alias || piece.right.name)]
224
+ else # "Normal" setup, fed from a JoinSource which has an array of JOINs
225
+ # The left side is the "JOIN" table
226
+ names += _recurse_arel(piece.left)
227
+ # The expression on the right side is the "ON" clause
228
+ # on = piece.right.expr
229
+ # # Find the table which is not ourselves, and thus must be the "path" that led us here
230
+ # parent = piece.left == on.left.relation ? on.right.relation : on.left.relation
231
+ # binding.pry if piece.left.is_a?(Arel::Nodes::TableAlias)
232
+ table = piece.left
233
+ if table.is_a?(Arel::Nodes::TableAlias)
234
+ alias_name = table.right
235
+ table = table.left
236
+ end
237
+ (_brick_chains[table._arel_table_type] ||= []) << (alias_name || table.table_alias || table.name)
238
+ # puts "YES! #{self.object_id}"
239
+ end
240
+ # rubocop:enable Style/IdenticalConditionalBranches
241
+ when Arel::Table # Table
242
+ names << [piece._arel_table_type, (piece.table_alias || piece.name)]
243
+ when Arel::Nodes::TableAlias # Alias
244
+ # Can get the real table name from: self._recurse_arel(piece.left)
245
+ names << [piece.left._arel_table_type, piece.right.to_s] # This is simply a string; the alias name itself
246
+ when Arel::Nodes::JoinSource # Leaving this until the end because AR < 3.2 doesn't know at all about JoinSource!
247
+ # Spin up an empty set of Brick alias name chains at the start
248
+ @_brick_chains = {}
249
+ # The left side is the "FROM" table
250
+ # names += _recurse_arel(piece.left)
251
+ names << [piece.left._arel_table_type, (piece.left.table_alias || piece.left.name)]
252
+ # The right side is an array of all JOINs
253
+ piece.right.each { |join| names << _recurse_arel(join) }
254
+ end
255
+ names
256
+ end
257
+
258
+ # INSTANCE STUFF
259
+ def _arel_alias_names
260
+ # %%% If with Rails 3.1 and older you get "NoMethodError: undefined method `eq' for nil:NilClass"
261
+ # when trying to call relation.arel, then somewhere along the line while navigating a has_many
262
+ # relationship it can't find the proper foreign key.
263
+ core = arel.ast.cores.first
264
+ # Accommodate AR < 3.2
265
+ if core.froms.is_a?(Arel::Table)
266
+ # All recent versions of AR have #source which brings up an Arel::Nodes::JoinSource
267
+ _recurse_arel(core.source)
268
+ else
269
+ # With AR < 3.2, "froms" brings up the top node, an Arel::Nodes::InnerJoin
270
+ _recurse_arel(core.froms)
271
+ end
272
+ end
273
+
274
+ def brick_select(params, selects = nil, bt_descrip = {}, hm_counts = {}, join_array = ::Brick::JoinArray.new
275
+ # , is_add_bts, is_add_hms
276
+ )
277
+ is_add_bts = is_add_hms = true
278
+
279
+ # %%% Skip the metadata columns
280
+ if selects&.empty? # Default to all columns
281
+ columns.each do |col|
282
+ selects << "#{table.name}.#{col.name}"
283
+ end
284
+ end
285
+
286
+ # Search for BT, HM, and HMT DSL stuff
287
+ translations = {}
288
+ if is_add_bts || is_add_hms
289
+ bts, hms, associatives = ::Brick.get_bts_and_hms(klass)
290
+ bts.each do |_k, bt|
291
+ # join_array will receive this relation name when calling #brick_parse_dsl
292
+ bt_descrip[bt.first] = [bt.last, bt.last.brick_parse_dsl(join_array, bt.first, translations)]
293
+ end
294
+ skip_klass_hms = ::Brick.config.skip_index_hms[klass.name] || {}
295
+ hms.each do |k, hm|
296
+ next if skip_klass_hms.key?(k)
297
+
298
+ hm_counts[k] = hm
299
+ end
300
+ end
301
+
110
302
  wheres = {}
111
- rel_joins = []
112
303
  params.each do |k, v|
113
304
  case (ks = k.split('.')).length
114
305
  when 1
@@ -116,23 +307,87 @@ module ActiveRecord
116
307
  when 2
117
308
  assoc_name = ks.first.to_sym
118
309
  # 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)
310
+ next unless klass.reflect_on_association(assoc_name)&.klass&.columns&.any? { |col| col.name == ks.last }
120
311
 
121
- rel_joins << assoc_name unless rel_joins.include?(assoc_name)
312
+ join_array[assoc_name] = nil # Store this relation name in our special collection for .joins()
122
313
  end
123
314
  wheres[k] = v.split(',')
124
315
  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
316
+
317
+ if join_array.present?
318
+ left_outer_joins!(join_array) # joins!(join_array)
319
+ # Without working from a duplicate, touching the AREL ast tree sets the @arel instance variable, which causes the relation to be immutable.
320
+ (rel_dupe = dup)._arel_alias_names
321
+ core_selects = selects.dup
322
+ chains = rel_dupe._brick_chains
323
+ id_for_tables = Hash.new { |h, k| h[k] = [] }
324
+ field_tbl_names = Hash.new { |h, k| h[k] = {} }
325
+ bt_columns = bt_descrip.each_with_object([]) do |v, s|
326
+ tbl_name = field_tbl_names[v.first][v.last.first] ||= shift_or_first(chains[v.last.first])
327
+ if (id_col = v.last.first.primary_key) && !id_for_tables.key?(v.first) # was tbl_name
328
+ # Accommodate composite primary key by allowing id_col to come in as an array
329
+ (id_col.is_a?(Array) ? id_col : [id_col]).each do |id_part|
330
+ selects << "#{"#{tbl_name}.#{id_part}"} AS \"#{(id_alias = "_brfk_#{v.first}__#{id_part}")}\""
331
+ id_for_tables[v.first] << id_alias
332
+ end
333
+ v.last << id_for_tables[v.first]
334
+ end
335
+ if (col_name = v.last[1].last&.last)
336
+ field_tbl_name = nil
337
+ v.last[1].map { |x| [translations[x[0..-2].map(&:to_s).join('.')], x.last] }.each_with_index do |sel_col, idx|
338
+ field_tbl_name ||= field_tbl_names[v.first][sel_col.first] ||= shift_or_first(chains[sel_col.first])
339
+ # col_name is weak when there are multiple, using sel_col.last instead
340
+ selects << "#{"#{field_tbl_name}.#{sel_col.last}"} AS \"#{(col_alias = "_brfk_#{v.first}__#{sel_col.last}")}\""
341
+ v.last[1][idx] << col_alias
342
+ end
343
+ end
344
+ end
345
+ join_array.each do |assoc_name|
346
+ # %%% Need to support {user: :profile}
347
+ next unless assoc_name.is_a?(Symbol)
348
+
349
+ table_alias = shift_or_first(chains[klass = reflect_on_association(assoc_name)&.klass])
350
+ _assoc_names[assoc_name] = [table_alias, klass]
351
+ end
352
+ end
353
+ # Add derived table JOIN for the has_many counts
354
+ hm_counts.each do |k, hm|
355
+ associative = nil
356
+ count_column = if hm.options[:through]
357
+ fk_col = (associative = associatives[hm.name]).foreign_key
358
+ hm.foreign_key
359
+ else
360
+ fk_col = hm.foreign_key
361
+ hm.klass.primary_key || '*'
362
+ end
363
+ tbl_alias = "_br_#{hm.name}"
364
+ pri_tbl = hm.active_record
365
+ if fk_col.is_a?(Array) # Composite key?
366
+ on_clause = []
367
+ fk_col.each_with_index { |fk_col_part, idx| on_clause << "#{tbl_alias}.#{fk_col_part} = #{pri_tbl.table_name}.#{pri_tbl.primary_key[idx]}" }
368
+ joins!("LEFT OUTER
369
+ JOIN (SELECT #{fk_col.join(', ')}, COUNT(#{count_column}) AS _ct_ FROM #{associative&.name || hm.klass.table_name} GROUP BY #{(1..fk_col.length).to_a.join(', ')}) AS #{tbl_alias}
370
+ ON #{on_clause.join(' AND ')}")
371
+ else
372
+ joins!("LEFT OUTER
373
+ JOIN (SELECT #{fk_col}, COUNT(#{count_column}) AS _ct_ FROM #{associative&.name || hm.klass.table_name} GROUP BY 1) AS #{tbl_alias}
374
+ ON #{tbl_alias}.#{fk_col} = #{pri_tbl.table_name}.#{pri_tbl.primary_key}")
375
+ end
129
376
  end
377
+ where!(wheres) unless wheres.empty?
378
+ wheres unless wheres.empty? # Return the specific parameters that we did use
379
+ end
380
+
381
+ private
382
+
383
+ def shift_or_first(ary)
384
+ ary.length > 1 ? ary.shift : ary.first
130
385
  end
131
386
  end
132
387
 
133
388
  module Inheritance
134
389
  module ClassMethods
135
- private
390
+ private
136
391
 
137
392
  alias _brick_find_sti_class find_sti_class
138
393
  def find_sti_class(type_name)
@@ -145,10 +400,8 @@ module ActiveRecord
145
400
  module_prefixes = type_name.split('::')
146
401
  module_prefixes.unshift('') unless module_prefixes.first.blank?
147
402
  module_name = module_prefixes[0..-2].join('::')
148
- if ::Brick.config.sti_namespace_prefixes&.key?("::#{module_name}::") ||
149
- ::Brick.config.sti_namespace_prefixes&.key?("#{module_name}::")
150
- _brick_find_sti_class(type_name)
151
- elsif File.exist?(candidate_file = Rails.root.join('app/models' + module_prefixes.map(&:underscore).join('/') + '.rb'))
403
+ if (snp = ::Brick.config.sti_namespace_prefixes)&.key?("::#{module_name}::") || snp&.key?("#{module_name}::") ||
404
+ File.exist?(candidate_file = Rails.root.join('app/models' + module_prefixes.map(&:underscore).join('/') + '.rb'))
152
405
  _brick_find_sti_class(type_name) # Find this STI class normally
153
406
  else
154
407
  # Build missing prefix modules if they don't yet exist
@@ -227,10 +480,9 @@ class Object
227
480
  end
228
481
 
229
482
  relations = ::Brick.instance_variable_get(:@relations)[ActiveRecord::Base.connection_pool.object_id] || {}
230
- is_controllers_enabled = ::Brick.enable_controllers? || (ENV['RAILS_ENV'] || ENV['RACK_ENV']) == 'development'
231
- result = if is_controllers_enabled && class_name.end_with?('Controller') && (plural_class_name = class_name[0..-11]).length.positive?
483
+ result = if ::Brick.enable_controllers? && class_name.end_with?('Controller') && (plural_class_name = class_name[0..-11]).length.positive?
232
484
  # Otherwise now it's up to us to fill in the gaps
233
- if (model = ActiveSupport::Inflector.singularize(plural_class_name).constantize)
485
+ if (model = plural_class_name.singularize.constantize)
234
486
  # 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.
235
487
  build_controller(class_name, plural_class_name, model, relations)
236
488
  end
@@ -281,6 +533,7 @@ class Object
281
533
  end
282
534
  return
283
535
  end
536
+
284
537
  if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, nil))
285
538
  is_sti = true
286
539
  else
@@ -328,27 +581,30 @@ class Object
328
581
  # Do the bulk of the has_many / belongs_to processing, and store details about HMT so they can be done at the very last
329
582
  hmts = fks.each_with_object(Hash.new { |h, k| h[k] = [] }) do |fk, hmts|
330
583
  # The key in each hash entry (fk.first) is the constraint name
331
- assoc_name = (assoc = fk.last)[:assoc_name]
332
- inverse_assoc_name = assoc[:inverse]&.fetch(:assoc_name, nil)
584
+ inverse_assoc_name = (assoc = fk.last)[:inverse]&.fetch(:assoc_name, nil)
333
585
  options = {}
334
586
  singular_table_name = ActiveSupport::Inflector.singularize(assoc[:inverse_table])
335
587
  macro = if assoc[:is_bt]
336
588
  # Try to take care of screwy names if this is a belongs_to going to an STI subclass
337
- if (primary_class = assoc.fetch(:primary_class, nil)) &&
338
- (sti_inverse_assoc = primary_class.reflect_on_all_associations.find { |a| a.macro == :has_many && a.options[:class_name] == self.name && assoc[:fk] = a.foreign_key })
339
- assoc_name = sti_inverse_assoc.options[:inverse_of].to_s || assoc_name
340
- end
589
+ assoc_name = if (primary_class = assoc.fetch(:primary_class, nil)) &&
590
+ sti_inverse_assoc = primary_class.reflect_on_all_associations.find do |a|
591
+ a.macro == :has_many && a.options[:class_name] == self.name && assoc[:fk] = a.foreign_key
592
+ end
593
+ sti_inverse_assoc.options[:inverse_of]&.to_s || assoc_name
594
+ else
595
+ assoc[:assoc_name]
596
+ end
341
597
  need_class_name = singular_table_name.underscore != assoc_name
342
598
  need_fk = "#{assoc_name}_id" != assoc[:fk]
343
599
  if (inverse = assoc[:inverse])
344
600
  inverse_assoc_name, _x = _brick_get_hm_assoc_name(relations[assoc[:inverse_table]], inverse)
345
601
  if (has_ones = ::Brick.config.has_ones&.fetch(inverse[:alternate_name].camelize, nil))&.key?(singular_inv_assoc_name = ActiveSupport::Inflector.singularize(inverse_assoc_name))
346
602
  inverse_assoc_name = if has_ones[singular_inv_assoc_name]
347
- need_inverse_of = true
348
- has_ones[singular_inv_assoc_name]
349
- else
350
- singular_inv_assoc_name
351
- end
603
+ need_inverse_of = true
604
+ has_ones[singular_inv_assoc_name]
605
+ else
606
+ singular_inv_assoc_name
607
+ end
352
608
  end
353
609
  end
354
610
  :belongs_to
@@ -359,12 +615,12 @@ class Object
359
615
  need_fk = "#{ActiveSupport::Inflector.singularize(assoc[:inverse][:inverse_table])}_id" != assoc[:fk]
360
616
  # fks[table_name].find { |other_assoc| other_assoc.object_id != assoc.object_id && other_assoc[:assoc_name] == assoc[assoc_name] }
361
617
  if (has_ones = ::Brick.config.has_ones&.fetch(model_name, nil))&.key?(singular_assoc_name = ActiveSupport::Inflector.singularize(assoc_name))
362
- assoc_name = if has_ones[singular_assoc_name]
363
- need_class_name = true
364
- has_ones[singular_assoc_name]
365
- else
366
- singular_assoc_name
367
- end
618
+ assoc_name = if (custom_assoc_name = has_ones[singular_assoc_name])
619
+ need_class_name = custom_assoc_name != singular_assoc_name
620
+ custom_assoc_name
621
+ else
622
+ singular_assoc_name
623
+ end
368
624
  :has_one
369
625
  else
370
626
  :has_many
@@ -442,13 +698,21 @@ class Object
442
698
 
443
699
  code << " def index\n"
444
700
  code << " @#{table_name} = #{model.name}#{model.primary_key ? ".order(#{model.primary_key.inspect})" : '.all'}\n"
445
- code << " @#{table_name}.brick_where(params)\n"
701
+ code << " @#{table_name}.brick_select(params)\n"
446
702
  code << " end\n"
447
703
  self.define_method :index do
448
704
  ::Brick.set_db_schema(params)
449
- ar_relation = model.primary_key ? model.order(model.primary_key) : model.all
450
- instance_variable_set(:@_brick_params, ar_relation.brick_where(params))
451
- instance_variable_set("@#{table_name}".to_sym, ar_relation)
705
+ ar_relation = model.all # model.primary_key ? model.order(model.primary_key) : model.all
706
+ @_brick_params = ar_relation.brick_select(params, (selects = []), (bt_descrip = {}), (hm_counts = {}), (join_array = ::Brick::JoinArray.new))
707
+ # %%% Add custom HM count columns
708
+ # %%% What happens when the PK is composite?
709
+ counts = hm_counts.each_with_object([]) { |v, s| s << "_br_#{v.first}._ct_ AS _br_#{v.first}_ct" }
710
+ # *selects,
711
+ instance_variable_set("@#{table_name}".to_sym, ar_relation.dup._select!(*selects, *counts))
712
+ # binding.pry
713
+ @_brick_bt_descrip = bt_descrip
714
+ @_brick_hm_counts = hm_counts
715
+ @_brick_join_array = join_array
452
716
  end
453
717
 
454
718
  if model.primary_key
@@ -500,7 +764,8 @@ class Object
500
764
 
501
765
  def _brick_get_hm_assoc_name(relation, hm_assoc)
502
766
  if relation[:hm_counts][hm_assoc[:assoc_name]]&.> 1
503
- [ActiveSupport::Inflector.pluralize(hm_assoc[:alternate_name]), true]
767
+ plural = ActiveSupport::Inflector.pluralize(hm_assoc[:alternate_name])
768
+ [hm_assoc[:alternate_name] == name.underscore ? "#{hm_assoc[:assoc_name].singularize}_#{plural}" : plural, true]
504
769
  else
505
770
  [ActiveSupport::Inflector.pluralize(hm_assoc[:inverse_table]), nil]
506
771
  end
@@ -757,18 +1022,16 @@ module Brick
757
1022
 
758
1023
  return if is_class || ::Brick.config.exclude_hms&.any? { |exclusion| fk[0] == exclusion[0] && fk[1] == exclusion[1] && primary_table == exclusion[2] }
759
1024
 
760
- cnstr_name = "hm_#{cnstr_name}"
761
- if (assoc_hm = hms.fetch(cnstr_name, nil))
1025
+ if (assoc_hm = hms.fetch((hm_cnstr_name = "hm_#{cnstr_name}"), nil))
762
1026
  assoc_hm[:fk] = assoc_hm[:fk].is_a?(String) ? [assoc_hm[:fk], fk[1]] : assoc_hm[:fk].concat(fk[1])
763
1027
  assoc_hm[:alternate_name] = "#{assoc_hm[:alternate_name]}_#{bt_assoc_name}" unless assoc_hm[:alternate_name] == bt_assoc_name
764
1028
  assoc_hm[:inverse] = assoc_bt
765
1029
  else
766
- 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 }
1030
+ assoc_hm = hms[hm_cnstr_name] = { is_bt: false, fk: fk[1], assoc_name: fk[0], alternate_name: bt_assoc_name, inverse_table: fk[0], inverse: assoc_bt }
767
1031
  hm_counts = relation.fetch(:hm_counts) { relation[:hm_counts] = {} }
768
1032
  hm_counts[fk[0]] = hm_counts.fetch(fk[0]) { 0 } + 1
769
1033
  end
770
1034
  assoc_bt[:inverse] = assoc_hm
771
- # hms[cnstr_name] << { is_bt: false, fk: fk[1], assoc_name: fk[0], alternate_name: bt_assoc_name, inverse_table: fk[0] }
772
1035
  end
773
1036
  end
774
1037
  end