brick 1.0.19 → 1.0.20

Sign up to get free protection for your applications and to get access to all the features.
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