brick 1.0.19 → 1.0.20

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: 41b64e2d571540382ce5ecd497f1e1413fde20119d46f1b24e1fae5af428fb45
4
+ data.tar.gz: 1c26ccf7b8df54fd121546e8daeeb9508561328186af46aea8ba1bd819524532
5
5
  SHA512:
6
- metadata.gz: 2b785c34e1a21563232ae54f6af25989fe02b73f92eb1327b27d6f21e80b7e353a5995305c4c1188dda885465a196f87fb5979a13c3e6a0219d4895dc59593dd
7
- data.tar.gz: bd2018d48dad15f51d7a87e39d2710bde9a5becbe969e741004bf730f57faecbd84a81a2f75cfdd60c2ff48f239fe6c5c8d3f47b4b2f79e355b7f100f9bb7943
6
+ metadata.gz: e103d061b74402f3fe0b944d06b30c20f053166c706a54f15753afc8cb0c703ae71f83bd88191f985a37180cb9bdafdba918178174df08f58f683a723fcfb73a
7
+ data.tar.gz: 13a7dce7f4f5789a0d7a0c6cae4adcaf2f44d22d0f59c355d75102a7f08292d80c781f8d33be121801d375fd33fed8bcdf4ef6b56108ef8f4ab4110c1f49e296
@@ -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
59
78
  end
60
- if (dsl ||= ::Brick.config.model_descrips[klass.name])
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
116
+ end
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,23 +276,86 @@ 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
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
+ hms.each do |k, hm|
308
+ join_array[k] = nil # Store this relation name in our special collection for .joins()
309
+ hm_counts[k] = nil # Placeholder that will be filled in once we know the proper table alias
310
+ end
129
311
  end
312
+ where!(wheres) unless wheres.empty?
313
+ if join_array.present?
314
+ left_outer_joins!(join_array) # joins!(join_array)
315
+ # Without working from a duplicate, touching the AREL ast tree sets the @arel instance variable, which causes the relation to be immutable.
316
+ (rel_dupe = dup)._arel_alias_names
317
+ core_selects = selects.dup
318
+ groups = []
319
+ chains = rel_dupe._brick_chains
320
+ id_for_tables = {}
321
+ bt_columns = bt_descrip.each_with_object([]) do |v, s|
322
+ tbl_name = chains[v.last.first].first
323
+ if (id_col = v.last.first.primary_key) && !id_for_tables.key?(tbl_name)
324
+ groups << (unaliased = "#{tbl_name}.#{id_col}")
325
+ selects << "#{unaliased} AS \"#{(id_alias = id_for_tables[tbl_name] = "_brfk_#{v.first}__#{id_col}")}\""
326
+ v.last << id_alias
327
+ end
328
+ if (col_name = v.last[1].last&.last)
329
+ v.last[1].map { |x| [translations[x[0..-2].map(&:to_s).join('.')], x.last] }.each_with_index do |sel_col, idx|
330
+ groups << (unaliased = "#{tbl_name = chains[sel_col.first].first}.#{sel_col.last}")
331
+ # col_name is weak when there are multiple, using sel_col.last instead
332
+ tbl_name2 = tbl_name.start_with?('public.') ? tbl_name[7..-1] : tbl_name
333
+ selects << "#{unaliased} AS \"#{(col_alias = "_brfk_#{tbl_name2}__#{sel_col.last}")}\""
334
+ v.last[1][idx] << col_alias
335
+ end
336
+ end
337
+ end
338
+ group!(core_selects + groups) if hm_counts.any? # + bt_columns
339
+ join_array.each do |assoc_name|
340
+ # %%% Need to support {user: :profile}
341
+ next unless assoc_name.is_a?(Symbol)
342
+
343
+ klass = reflect_on_association(assoc_name)&.klass
344
+ table_alias = chains[klass].length > 1 ? chains[klass].shift : chains[klass].first
345
+ _assoc_names[assoc_name] = [table_alias, klass]
346
+ end
347
+ # Copy entries over
348
+ hm_counts.keys.each do |k|
349
+ hm_counts[k] = _assoc_names[k]
350
+ end
351
+ end
352
+ wheres unless wheres.empty? # Return the specific parameters that we did use
130
353
  end
131
354
  end
132
355
 
133
356
  module Inheritance
134
357
  module ClassMethods
135
- private
358
+ private
136
359
 
137
360
  alias _brick_find_sti_class find_sti_class
138
361
  def find_sti_class(type_name)
@@ -145,10 +368,8 @@ module ActiveRecord
145
368
  module_prefixes = type_name.split('::')
146
369
  module_prefixes.unshift('') unless module_prefixes.first.blank?
147
370
  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'))
371
+ if (snp = ::Brick.config.sti_namespace_prefixes)&.key?("::#{module_name}::") || snp&.key?("#{module_name}::") ||
372
+ File.exist?(candidate_file = Rails.root.join('app/models' + module_prefixes.map(&:underscore).join('/') + '.rb'))
152
373
  _brick_find_sti_class(type_name) # Find this STI class normally
153
374
  else
154
375
  # Build missing prefix modules if they don't yet exist
@@ -227,10 +448,9 @@ class Object
227
448
  end
228
449
 
229
450
  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?
451
+ result = if ::Brick.enable_controllers? && class_name.end_with?('Controller') && (plural_class_name = class_name[0..-11]).length.positive?
232
452
  # Otherwise now it's up to us to fill in the gaps
233
- if (model = ActiveSupport::Inflector.singularize(plural_class_name).constantize)
453
+ if (model = plural_class_name.singularize.constantize)
234
454
  # 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
455
  build_controller(class_name, plural_class_name, model, relations)
236
456
  end
@@ -281,6 +501,7 @@ class Object
281
501
  end
282
502
  return
283
503
  end
504
+
284
505
  if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, nil))
285
506
  is_sti = true
286
507
  else
@@ -442,13 +663,22 @@ class Object
442
663
 
443
664
  code << " def index\n"
444
665
  code << " @#{table_name} = #{model.name}#{model.primary_key ? ".order(#{model.primary_key.inspect})" : '.all'}\n"
445
- code << " @#{table_name}.brick_where(params)\n"
666
+ code << " @#{table_name}.brick_select(params)\n"
446
667
  code << " end\n"
447
668
  self.define_method :index do
448
669
  ::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)
670
+ ar_relation = model.all # model.primary_key ? model.order(model.primary_key) : model.all
671
+ @_brick_params = ar_relation.brick_select(params, (selects = []), (bt_descrip = {}), (hm_counts = {}), (join_array = ::Brick::JoinArray.new))
672
+ # %%% Add custom HM count columns
673
+ # %%% What happens when the PK is composite?
674
+ counts = hm_counts.each_with_object([]) { |v, s| s << "COUNT(DISTINCT #{v.last.first}.#{v.last.last.primary_key}) AS _br_#{v.first}_ct" }
675
+ puts counts.inspect
676
+ # *selects,
677
+ instance_variable_set("@#{table_name}".to_sym, ar_relation.dup._select!(*selects, *counts))
678
+ # binding.pry
679
+ @_brick_bt_descrip = bt_descrip
680
+ @_brick_hm_counts = hm_counts
681
+ @_brick_join_array = join_array
452
682
  end
453
683
 
454
684
  if model.primary_key
@@ -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)
@@ -52,10 +54,11 @@ module Brick
52
54
  # args will be something like: ["index", ["categories"]]
53
55
  model = args[1].map(&:camelize).join('::').singularize.constantize
54
56
  if is_template_exists = model && (
55
- ['index', 'show'].include?(args.first) || # Everything has index and show
56
- # Only CRU stuff has create / update / destroy
57
- (!model.is_view? && ['new', 'create', 'edit', 'update', 'destroy'].include?(args.first))
58
- ) && instance_variable_set(:@_brick_model, 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
59
62
  end
60
63
  end
61
64
  is_template_exists
@@ -63,59 +66,43 @@ module Brick
63
66
 
64
67
  alias :_brick_find_template :find_template
65
68
  def find_template(*args, **options)
66
- if @_brick_model
67
- model_name = @_brick_model.name
68
- pk = @_brick_model.primary_key
69
- obj_name = model_name.underscore
70
- table_name = model_name.pluralize.underscore
71
- # This gets has_many as well as has_many :through
72
- # %%% weed out ones that don't have an available model to reference
73
- bts, hms = ::Brick.get_bts_and_hms(@_brick_model)
74
- # Mark has_manys that go to an associative ("join") table so that they are skipped in the UI,
75
- # as well as any possible polymorphic associations
76
- exclude_hms = {}
77
- associatives = hms.each_with_object({}) do |hmt, s|
78
- if (through = hmt.last.options[:through])
79
- exclude_hms[through] = nil
80
- s[hmt.first] = hms[through] # End up with a hash of HMT names pointing to join-table associations
81
- elsif hmt.last.inverse_of.nil?
82
- puts "SKIPPING #{hmt.last.name.inspect}"
83
- # %%% If we don't do this then below associative.name will find that associative is nil
84
- exclude_hms[hmt.last.name] = nil
85
- end
86
- end
87
-
88
- hms_columns = +'' # Used for 'index'
89
- hms_headers = hms.each_with_object([]) do |hm, s|
90
- next if exclude_hms.key?((hm_assoc = hm.last).name)
69
+ return _brick_find_template(*args, **options) unless @_brick_model
91
70
 
92
- if args.first == 'index'
93
- hm_fk_name = if hm_assoc.options[:through]
94
- associative = associatives[hm_assoc.name]
95
- "'#{associative.name}.#{associative.foreign_key}'"
96
- else
97
- hm_assoc.foreign_key
98
- end
99
- hms_columns << if hm_assoc.macro == :has_many
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
+ hms_headers = hms.each_with_object([]) do |hm, s|
78
+ hm_assoc = hm.last
79
+ if args.first == 'index'
80
+ hm_fk_name = if hm_assoc.options[:through]
81
+ associative = associatives[hm_assoc.name]
82
+ "'#{associative.name}.#{associative.foreign_key}'"
83
+ else
84
+ hm_assoc.foreign_key
85
+ end
86
+ hms_columns << if hm_assoc.macro == :has_many
100
87
  "<td>
101
- <%= 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? %>
88
+ <%= ct = #{obj_name}._br_#{hm.first}_ct
89
+ link_to \"#\{ct\} #{hm.first}\", #{hm_assoc.klass.name.underscore.pluralize}_path({ #{hm_fk_name}: #{obj_name}.#{pk} }) unless ct.zero? %>
102
90
  </td>\n"
103
- else # has_one
91
+ else # has_one
104
92
  "<td>
105
93
  <%= obj = #{obj_name}.#{hm.first}; link_to(obj.brick_descrip, obj) if obj %>
106
94
  </td>\n"
107
- end
108
- end
109
- s << [hm_assoc, "H#{hm_assoc.macro == :has_one ? 'O' : 'M'}#{'T' if hm_assoc.options[:through]} #{hm.first}"]
95
+ end
110
96
  end
97
+ s << [hm_assoc, "H#{hm_assoc.macro == :has_one ? 'O' : 'M'}#{'T' if hm_assoc.options[:through]} #{hm.first}"]
111
98
  end
112
- if @_brick_model
113
- schema_options = ::Brick.db_schemas.each_with_object(+'') { |v, s| s << "<option value=\"#{v}\">#{v}</option>" }.html_safe
114
- # %%% If we are not auto-creating controllers (or routes) then omit by default, and if enabled anyway, such as in a development
115
- # environment or whatever, then get either the controllers or routes list instead
116
- table_options = (::Brick.relations.keys - ::Brick.config.exclude_tables)
117
- .each_with_object(+'') { |v, s| s << "<option value=\"#{v.underscore.pluralize}\">#{v}</option>" }.html_safe
118
- css = "<style>
99
+
100
+ schema_options = ::Brick.db_schemas.each_with_object(+'') { |v, s| s << "<option value=\"#{v}\">#{v}</option>" }.html_safe
101
+ # %%% If we are not auto-creating controllers (or routes) then omit by default, and if enabled anyway, such as in a development
102
+ # environment or whatever, then get either the controllers or routes list instead
103
+ table_options = (::Brick.relations.keys - ::Brick.config.exclude_tables)
104
+ .each_with_object(+'') { |v, s| s << "<option value=\"#{v.underscore.pluralize}\">#{v}</option>" }.html_safe
105
+ css = +"<style>
119
106
  table {
120
107
  border-collapse: collapse;
121
108
  margin: 25px 0;
@@ -189,7 +176,12 @@ def hide_bcrypt(val)
189
176
  is_bcrypt?(val) ? '(hidden)' : val
190
177
  end %>"
191
178
 
192
- script = "<script>
179
+ if ['index', 'show', 'update'].include?(args.first)
180
+ # Example: <% bts = { "site_id" => [:site, Site, "id"], "study_id" => [:study, Study, "id"], "study_country_id" => [:study_country, StudyCountry, "id"], "user_id" => [:user, User, "id"], "role_id" => [:role, Role, "id"] } %>
181
+ 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(', ')} } %>"
182
+ end
183
+
184
+ script = "<script>
193
185
  var schemaSelect = document.getElementById(\"schema\");
194
186
  var brickSchema;
195
187
  if (schemaSelect) {
@@ -243,9 +235,8 @@ function changeout(href, param, value) {
243
235
  return hrefParts[0] + \"?\" + Object.keys(params).reduce(function (s, v) { s.push(v + \"=\" + params[v]); return s; }, []).join(\"&\");
244
236
  }
245
237
  </script>"
246
-
247
- inline = case args.first
248
- when 'index'
238
+ inline = case args.first
239
+ when 'index'
249
240
  "#{css}
250
241
  <p style=\"color: green\"><%= notice %></p>#{"
251
242
  <select id=\"schema\">#{schema_options}</select>" if ::Brick.db_schemas.length > 1}
@@ -253,9 +244,8 @@ function changeout(href, param, value) {
253
244
  <h1>#{model_name.pluralize}</h1>
254
245
  <% if @_brick_params&.present? %><h3>where <%= @_brick_params.each_with_object([]) { |v, s| s << \"#\{v.first\} = #\{v.last.inspect\}\" }.join(', ') %></h3><% end %>
255
246
  <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| %>
247
+ <thead><tr>#{'<th></th>' if pk}
248
+ <% @#{table_name}.columns.map(&:name).each do |col| %>
259
249
  <% next if col == '#{pk}' || ::Brick.config.metadata_columns.include?(col) %>
260
250
  <th>
261
251
  <% if (bt = bts[col]) %>
@@ -273,14 +263,14 @@ function changeout(href, param, value) {
273
263
  <tr>#{"
274
264
  <td><%= link_to '⇛', #{obj_name}_path(#{obj_name}.#{pk}), { class: 'big-arrow' } %></td>" if pk}
275
265
  <% #{obj_name}.attributes.each do |k, val| %>
276
- <% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) %>
266
+ <% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) || k.start_with?('_brfk_') || (k.start_with?('_br_') && k.end_with?('_ct')) %>
277
267
  <td>
278
268
  <% 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 %>
269
+ <%# binding.pry if bt.first == :user %>
270
+ <% 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]) %>
271
+ <% bt_id_col = @_brick_bt_descrip[bt.first][2]; bt_id = #{obj_name}.send(bt_id_col) if bt_id_col %>
272
+ <%= bt_id ? link_to(bt_txt, send(\"#\{bt_obj_path_base = bt[1].name.underscore\}_path\".to_sym, bt_id)) : bt_txt %>
273
+ <%#= 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
274
  <% else %>
285
275
  <%= hide_bcrypt(val) %>
286
276
  <% end %>
@@ -295,7 +285,7 @@ function changeout(href, param, value) {
295
285
 
296
286
  #{"<hr><%= link_to \"New #{obj_name}\", new_#{obj_name}_path %>" unless @_brick_model.is_view?}
297
287
  #{script}"
298
- when 'show', 'update'
288
+ when 'show', 'update'
299
289
  "#{css}
300
290
  <p style=\"color: green\"><%= notice %></p>#{"
301
291
  <select id=\"schema\">#{schema_options}</select>" if ::Brick.db_schemas.length > 1}
@@ -308,8 +298,7 @@ function changeout(href, param, value) {
308
298
  # url = send(:#{model_name.underscore}_path, obj.#{pk})
309
299
  form_for(obj) do |f| %>
310
300
  <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| %>
301
+ <% @#{obj_name}.first.attributes.each do |k, val| %>
313
302
  <tr>
314
303
  <% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) %>
315
304
  <th class=\"show-field\">
@@ -331,7 +320,7 @@ function changeout(href, param, value) {
331
320
  html_options = { prompt: \"Select #\{bt_name\}\" }
332
321
  html_options[:class] = 'dimmed' unless val %>
333
322
  <%= 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 %>
323
+ <%= 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
324
  <% else case #{model_name}.column_for_attribute(k).type
336
325
  when :string, :text %>
337
326
  <% if is_bcrypt?(val) # || .readonly? %>
@@ -342,10 +331,10 @@ function changeout(href, param, value) {
342
331
  <% when :boolean %>
343
332
  <%= f.check_box k.to_sym %>
344
333
  <% when :integer, :decimal, :float, :date, :datetime, :time, :timestamp
345
- # What happens when keys are UUID?
346
- # Postgres naturally uses the +uuid_generate_v4()+ function from the uuid-ossp extension
347
- # If it's not yet enabled then: enable_extension 'uuid-ossp'
348
- # ActiveUUID gem created a new :uuid type %>
334
+ # What happens when keys are UUID?
335
+ # Postgres naturally uses the +uuid_generate_v4()+ function from the uuid-ossp extension
336
+ # If it's not yet enabled then: enable_extension 'uuid-ossp'
337
+ # ActiveUUID gem created a new :uuid type %>
349
338
  <%= val %>
350
339
  <% when :binary, :primary_key %>
351
340
  <% end %>
@@ -359,6 +348,7 @@ function changeout(href, param, value) {
359
348
 
360
349
  #{hms_headers.map do |hm|
361
350
  next unless (pk = hm.first.klass.primary_key)
351
+
362
352
  "<table id=\"#{hm_name = hm.first.name.to_s}\">
363
353
  <tr><th>#{hm.last}</th></tr>
364
354
  <% collection = @#{obj_name}.first.#{hm_name}
@@ -374,19 +364,16 @@ function changeout(href, param, value) {
374
364
  <% end %>
375
365
  #{script}"
376
366
 
377
- end
378
- # As if it were an inline template (see #determine_template in actionview-5.2.6.2/lib/action_view/renderer/template_renderer.rb)
379
- keys = options.has_key?(:locals) ? options[:locals].keys : []
380
- handler = ActionView::Template.handler_for_extension(options[:type] || 'erb')
381
- ActionView::Template.new(inline, "auto-generated #{args.first} template", handler, locals: keys)
382
- else
383
- _brick_find_template(*args, **options)
384
- end
367
+ end
368
+ # As if it were an inline template (see #determine_template in actionview-5.2.6.2/lib/action_view/renderer/template_renderer.rb)
369
+ keys = options.has_key?(:locals) ? options[:locals].keys : []
370
+ handler = ActionView::Template.handler_for_extension(options[:type] || 'erb')
371
+ ActionView::Template.new(inline, "auto-generated #{args.first} template", handler, locals: keys)
385
372
  end
386
373
  end
387
374
  end
388
375
 
389
- if ::Brick.enable_routes? || (ENV['RAILS_ENV'] || ENV['RACK_ENV']) == 'development'
376
+ if ::Brick.enable_routes?
390
377
  ActionDispatch::Routing::RouteSet.class_exec do
391
378
  # In order to defer auto-creation of any routes that already exist, calculate Brick routes only after having loaded all others
392
379
  prepend ::Brick::RouteSet
@@ -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 = 19
8
+ TINY = 20
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
+ # So that we can map an association name to any special alias name used in an AREL query
107
+ ans = (model._assoc_names[a.name] ||= [])
108
+ next if !const_defined?(a.name.to_s.singularize.camelize) && ::Brick.config.exclude_tables.include?(a.plural_name)
109
+
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
@@ -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.)
@@ -159,7 +159,7 @@ module Brick
159
159
  # Brick.default_route_fallback = 'customers' # This defaults to \"customers/index\"
160
160
  # Brick.default_route_fallback = 'orders/outstanding' # Example of a non-RESTful route
161
161
  # Brick.default_route_fallback = '' # Omits setting a default route in the absence of any other
162
- "
162
+ ")
163
163
  end
164
164
  end
165
165
 
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.19
4
+ version: 1.0.20
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-04-09 00:00:00.000000000 Z
11
+ date: 2022-04-29 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