brick 1.0.2 → 1.0.5

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: 36447b749972f091f6d4c7c9127a7abfd5291d159f4ab81e0fb423feae7c2c03
4
- data.tar.gz: eb2d1c36e67dd692ac765198c132c801c8633e32ecd199f42605f7659e7f3abd
3
+ metadata.gz: 9cd49783dae6701760920fe007cbde7382fd3dbaf5e8f7472d475c0fe8aaf660
4
+ data.tar.gz: d2330f28e87dde00b1377f6cbca31cbbfe2dd3b2c495f1fbd0aebc0f8a951504
5
5
  SHA512:
6
- metadata.gz: da45315cae612125917448549f3be6cf163c6dc30ac97794f57544321b2c21536600903e9eb30a379fc6a0845656fe7704df863db13ad62a70c623f4f9cba454
7
- data.tar.gz: 64ec86f256752004d07447e94788e453f3e6cf9006082629f5e46791b36d6ce8031e22753217d5f23be5b672f65b35ddb9a55b696db5de0bb85686f854154fee
6
+ metadata.gz: 2a3c7ba47d32f3a6ab6c9be838006826f5266efb1349d90ce7d53b5e8f89ea078bdcb699a7db8084176e2de444a27931ce8e0216af666c25704c0568b59ac827
7
+ data.tar.gz: 41fccf3387315cd54fd596955c3f07760941cd4aa51959dc99f87d0b6fb38de742ad11fe7dabb084a3e72167c340b8aafce838c7704626359ccb41878e202ae2
data/lib/brick/config.rb CHANGED
@@ -65,6 +65,15 @@ module Brick
65
65
  @mutex.synchronize { @additional_references = references }
66
66
  end
67
67
 
68
+ # Associations to treat has a has_one
69
+ def has_ones
70
+ @mutex.synchronize { @has_ones }
71
+ end
72
+
73
+ def has_ones=(references)
74
+ @mutex.synchronize { @has_ones = references }
75
+ end
76
+
68
77
  def skip_database_views
69
78
  @mutex.synchronize { @skip_database_views }
70
79
  end
@@ -26,7 +26,11 @@
26
26
 
27
27
  # colour coded origins
28
28
 
29
- # Drag TmfModel#name onto the rows and have it automatically add five columns -- where type=zone / where type = sectionn / etc
29
+ # Drag something like TmfModel#name onto the rows and have it automatically add five columns -- where type=zone / where type = section / etc
30
+
31
+ # Support for Postgres / MySQL enums (add enum to model, use model enums to make a drop-down in the UI)
32
+
33
+ # Currently quadrupling up routes
30
34
 
31
35
  # ==========================================================
32
36
  # Dynamically create model or controller classes when needed
@@ -55,13 +59,23 @@ module ActiveRecord
55
59
  class Relation
56
60
  def brick_where(params)
57
61
  wheres = {}
62
+ rel_joins = []
58
63
  params.each do |k, v|
59
- next unless klass._brick_get_fks.include?(k)
60
-
64
+ case (ks = k.split('.')).length
65
+ when 1
66
+ next unless klass._brick_get_fks.include?(k)
67
+ when 2
68
+ assoc_name = ks.first.to_sym
69
+ # Make sure it's a good association name and that the model has that column name
70
+ next unless klass.reflect_on_association(assoc_name)&.klass&.columns&.map(&:name)&.include?(ks.last)
71
+
72
+ rel_joins << assoc_name unless rel_joins.include?(assoc_name)
73
+ end
61
74
  wheres[k] = v.split(',')
62
75
  end
63
76
  unless wheres.empty?
64
77
  where!(wheres)
78
+ joins!(rel_joins) unless rel_joins.empty?
65
79
  wheres # Return the specific parameters that we did use
66
80
  end
67
81
  end
@@ -161,15 +175,6 @@ class Object
161
175
  code << " # Could not identify any column(s) to use as a primary key\n" unless is_view
162
176
  end
163
177
 
164
- # if relation[:cols].key?('last_update')
165
- # define_method :updated_at do
166
- # last_update
167
- # end
168
- # define_method :'updated_at=' do |val|
169
- # last_update=(val)
170
- # end
171
- # end
172
-
173
178
  fks = relation[:fks] || {}
174
179
  fks.each do |_constraint_name, assoc|
175
180
  assoc_name = assoc[:assoc_name]
@@ -180,21 +185,38 @@ class Object
180
185
  need_class_name = singular_table_name.underscore != assoc_name
181
186
  need_fk = "#{assoc_name}_id" != assoc[:fk]
182
187
  inverse_assoc_name, _x = _brick_get_hm_assoc_name(relations[assoc[:inverse_table]], assoc[:inverse])
188
+ if (has_ones = ::Brick.config.has_ones.fetch(assoc[:inverse][:alternate_name].camelize, nil))&.key?(singular_inv_assoc_name = ActiveSupport::Inflector.singularize(inverse_assoc_name))
189
+ inverse_assoc_name = if has_ones[singular_inv_assoc_name]
190
+ need_inverse_of = true
191
+ has_ones[singular_inv_assoc_name]
192
+ else
193
+ singular_inv_assoc_name
194
+ end
195
+ end
183
196
  :belongs_to
184
197
  else
185
198
  # need_class_name = ActiveSupport::Inflector.singularize(assoc_name) == ActiveSupport::Inflector.singularize(table_name.underscore)
186
199
  # Are there multiple foreign keys out to the same table?
187
200
  assoc_name, need_class_name = _brick_get_hm_assoc_name(relation, assoc)
188
- need_fk = "#{singular_table_name}_id" != assoc[:fk]
201
+ need_fk = "#{ActiveSupport::Inflector.singularize(assoc[:inverse][:inverse_table])}_id" != assoc[:fk]
189
202
  # fks[table_name].find { |other_assoc| other_assoc.object_id != assoc.object_id && other_assoc[:assoc_name] == assoc[assoc_name] }
190
- :has_many
203
+ if (has_ones = ::Brick.config.has_ones&.fetch(model_name, nil))&.key?(singular_assoc_name = ActiveSupport::Inflector.singularize(assoc_name))
204
+ assoc_name = if has_ones[singular_assoc_name]
205
+ need_class_name = true
206
+ has_ones[singular_assoc_name]
207
+ else
208
+ singular_assoc_name
209
+ end
210
+ :has_one
211
+ else
212
+ :has_many
213
+ end
191
214
  end
215
+ # Figure out if we need to specially call out the class_name and/or foreign key
216
+ # (and if either of those then definitely also a specific inverse_of)
192
217
  options[:class_name] = singular_table_name.camelize if need_class_name
193
- # Figure out if we need to specially call out the foreign key
194
- if need_fk # Funky foreign key?
195
- options[:foreign_key] = assoc[:fk].to_sym
196
- end
197
- options[:inverse_of] = inverse_assoc_name.to_sym if need_class_name || need_fk
218
+ options[:foreign_key] = assoc[:fk].to_sym if need_fk # Funky foreign key?
219
+ options[:inverse_of] = inverse_assoc_name.to_sym if need_class_name || need_fk || need_inverse_of
198
220
  assoc_name = assoc_name.to_sym
199
221
  code << " #{macro} #{assoc_name.inspect}#{options.map { |k, v| ", #{k}: #{v.inspect}" }.join}\n"
200
222
  self.send(macro, assoc_name, **options)
@@ -224,7 +246,7 @@ class Object
224
246
  Object.const_set(class_name.to_sym, new_controller_class)
225
247
 
226
248
  code << " def index\n"
227
- code << " @#{table_name} = #{model.name}#{model.primary_key ? ".order(#{model.primary_key.inspect}" : '.all'})\n"
249
+ code << " @#{table_name} = #{model.name}#{model.primary_key ? ".order(#{model.primary_key.inspect})" : '.all'}\n"
228
250
  code << " @#{table_name}.brick_where(params)\n"
229
251
  code << " end\n"
230
252
  self.define_method :index do
@@ -269,16 +291,38 @@ end
269
291
  module ActiveRecord::ConnectionHandling
270
292
  alias _brick_establish_connection establish_connection
271
293
  def establish_connection(*args)
272
- x = _brick_establish_connection(*args)
294
+ conn = _brick_establish_connection(*args)
295
+ _brick_reflect_tables
296
+ conn
297
+ end
298
+
299
+ def _brick_reflect_tables
300
+ if (relations = ::Brick.relations).empty?
301
+ # Only for Postgres? (Doesn't work in sqlite3)
302
+ # puts ActiveRecord::Base.connection.execute("SELECT current_setting('SEARCH_PATH')").to_a.inspect
303
+
304
+ case ActiveRecord::Base.connection.adapter_name
305
+ when 'PostgreSQL'
306
+ schema = 'public'
307
+ when 'Mysql2'
308
+ schema = ActiveRecord::Base.connection.current_database
309
+ when 'SQLite'
310
+ sql = "SELECT m.name AS relation_name, UPPER(m.type) AS table_type,
311
+ p.name AS column_name, p.type AS data_type,
312
+ CASE p.pk WHEN 1 THEN 'PRIMARY KEY' END AS const
313
+ FROM sqlite_master AS m
314
+ INNER JOIN pragma_table_info(m.name) AS p
315
+ WHERE m.name NOT IN ('ar_internal_metadata', 'schema_migrations')
316
+ ORDER BY m.name, p.cid"
317
+ else
318
+ puts "Unfamiliar with connection adapter #{ActiveRecord::Base.connection.adapter_name}"
319
+ end
273
320
 
274
- if (relations = ::Brick.relations).empty?
275
- schema = 'public'
276
- puts ActiveRecord::Base.connection.execute("SELECT current_setting('SEARCH_PATH')").to_a.inspect
277
- sql = ActiveRecord::Base.send(:sanitize_sql_array, [
321
+ sql ||= ActiveRecord::Base.send(:sanitize_sql_array, [
278
322
  "SELECT t.table_name AS relation_name, t.table_type,
279
323
  c.column_name, c.data_type,
280
324
  COALESCE(c.character_maximum_length, c.numeric_precision) AS max_length,
281
- tc.constraint_type AS const, kcu.constraint_name AS key
325
+ tc.constraint_type AS const, kcu.constraint_name AS \"key\"
282
326
  FROM INFORMATION_SCHEMA.tables AS t
283
327
  LEFT OUTER JOIN INFORMATION_SCHEMA.columns AS c ON t.table_schema = c.table_schema
284
328
  AND t.table_name = c.table_name
@@ -290,59 +334,98 @@ module ActiveRecord::ConnectionHandling
290
334
  AND kcu.ordinal_position = c.ordinal_position
291
335
  LEFT OUTER JOIN INFORMATION_SCHEMA.table_constraints AS tc
292
336
  ON kcu.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA
337
+ AND kcu.TABLE_NAME = tc.TABLE_NAME
293
338
  AND kcu.CONSTRAINT_NAME = tc.constraint_name
294
339
  WHERE t.table_schema = ? -- COALESCE(current_setting('SEARCH_PATH'), 'public')
295
340
  -- AND t.table_type IN ('VIEW') -- 'BASE TABLE', 'FOREIGN TABLE'
296
341
  AND t.table_name NOT IN ('pg_stat_statements', 'ar_internal_metadata', 'schema_migrations')
297
342
  ORDER BY 1, t.table_type DESC, c.ordinal_position", schema
298
343
  ])
299
- ActiveRecord::Base.connection.execute(sql).each do |r|
300
- # next if internal_views.include?(r['relation_name']) # Skip internal views such as v_all_assessments
301
-
302
- relation = relations[r['relation_name']]
303
- relation[:isView] = true if r['table_type'] == 'VIEW'
304
- col_name = r['column_name']
305
- cols = relation[:cols] # relation.fetch(:cols) { relation[:cols] = [] }
306
- key = case r['const']
307
- when 'PRIMARY KEY'
308
- relation[:pkey][r['key']] ||= []
309
- when 'UNIQUE'
310
- relation[:ukeys][r['key']] ||= []
311
- # key = (relation[:ukeys] = Hash.new { |h, k| h[k] = [] }) if key.is_a?(Array)
312
- # key[r['key']]
313
- end
314
- key << col_name if key
315
- cols[col_name] = [r['data_type'], r['max_length'], r['measures']&.include?(col_name)]
316
- # puts "KEY! #{r['relation_name']}.#{col_name} #{r['key']} #{r['const']}" if r['key']
344
+
345
+ measures = []
346
+ case ActiveRecord::Base.connection.adapter_name
347
+ when 'PostgreSQL', 'SQLite' # These bring back a hash for each row because the query uses column aliases
348
+ ActiveRecord::Base.connection.execute(sql).each do |r|
349
+ # next if internal_views.include?(r['relation_name']) # Skip internal views such as v_all_assessments
350
+ relation = relations[(relation_name = r['relation_name'])]
351
+ relation[:isView] = true if r['table_type'] == 'VIEW'
352
+ col_name = r['column_name']
353
+ key = case r['const']
354
+ when 'PRIMARY KEY'
355
+ relation[:pkey][r['key'] || relation_name] ||= []
356
+ when 'UNIQUE'
357
+ relation[:ukeys][r['key'] || "#{relation_name}.#{col_name}"] ||= []
358
+ # key = (relation[:ukeys] = Hash.new { |h, k| h[k] = [] }) if key.is_a?(Array)
359
+ # key[r['key']]
360
+ end
361
+ key << col_name if key
362
+ cols = relation[:cols] # relation.fetch(:cols) { relation[:cols] = [] }
363
+ cols[col_name] = [r['data_type'], r['max_length'], measures&.include?(col_name)]
364
+ # puts "KEY! #{r['relation_name']}.#{col_name} #{r['key']} #{r['const']}" if r['key']
365
+ end
366
+ else # MySQL2 acts a little differently, bringing back an array for each row
367
+ ActiveRecord::Base.connection.execute(sql).each do |r|
368
+ # next if internal_views.include?(r['relation_name']) # Skip internal views such as v_all_assessments
369
+ relation = relations[(relation_name = r[0])] # here relation represents a table or view from the database
370
+ relation[:isView] = true if r[1] == 'VIEW' # table_type
371
+ col_name = r[2]
372
+ key = case r[5] # constraint type
373
+ when 'PRIMARY KEY'
374
+ # key
375
+ relation[:pkey][r[6] || relation_name] ||= []
376
+ when 'UNIQUE'
377
+ relation[:ukeys][r[6] || "#{relation_name}.#{col_name}"] ||= []
378
+ # key = (relation[:ukeys] = Hash.new { |h, k| h[k] = [] }) if key.is_a?(Array)
379
+ # key[r['key']]
380
+ end
381
+ key << col_name if key
382
+ cols = relation[:cols] # relation.fetch(:cols) { relation[:cols] = [] }
383
+ # 'data_type', 'max_length'
384
+ cols[col_name] = [r[3], r[4], measures&.include?(col_name)]
385
+ # puts "KEY! #{r['relation_name']}.#{col_name} #{r['key']} #{r['const']}" if r['key']
386
+ end
317
387
  end
318
388
 
319
- sql = ActiveRecord::Base.send(:sanitize_sql_array, [
320
- "SELECT kcu1.TABLE_NAME, kcu1.COLUMN_NAME, kcu2.TABLE_NAME, kcu1.CONSTRAINT_NAME
321
- FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS rc
322
- INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu1
323
- ON kcu1.CONSTRAINT_CATALOG = rc.CONSTRAINT_CATALOG
324
- AND kcu1.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA
325
- AND kcu1.CONSTRAINT_NAME = rc.CONSTRAINT_NAME
326
- INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu2
327
- ON kcu2.CONSTRAINT_CATALOG = rc.UNIQUE_CONSTRAINT_CATALOG
328
- AND kcu2.CONSTRAINT_SCHEMA = rc.UNIQUE_CONSTRAINT_SCHEMA
329
- AND kcu2.CONSTRAINT_NAME = rc.UNIQUE_CONSTRAINT_NAME
330
- AND kcu2.ORDINAL_POSITION = kcu1.ORDINAL_POSITION
331
- WHERE kcu1.CONSTRAINT_SCHEMA = ? -- COALESCE(current_setting('SEARCH_PATH'), 'public')", schema
332
- # AND kcu2.TABLE_NAME = ?;", Apartment::Tenant.current, table_name
333
- ])
334
- ActiveRecord::Base.connection.execute(sql).values.each { |fk| ::Brick._add_bt_and_hm(fk, relations) }
389
+ case ActiveRecord::Base.connection.adapter_name
390
+ when 'PostgreSQL', 'Mysql2'
391
+ sql = ActiveRecord::Base.send(:sanitize_sql_array, [
392
+ "SELECT kcu1.TABLE_NAME, kcu1.COLUMN_NAME, kcu2.TABLE_NAME AS primary_table, kcu1.CONSTRAINT_NAME
393
+ FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS rc
394
+ INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu1
395
+ ON kcu1.CONSTRAINT_CATALOG = rc.CONSTRAINT_CATALOG
396
+ AND kcu1.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA
397
+ AND kcu1.CONSTRAINT_NAME = rc.CONSTRAINT_NAME
398
+ INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu2
399
+ ON kcu2.CONSTRAINT_CATALOG = rc.UNIQUE_CONSTRAINT_CATALOG
400
+ AND kcu2.CONSTRAINT_SCHEMA = rc.UNIQUE_CONSTRAINT_SCHEMA
401
+ AND kcu2.CONSTRAINT_NAME = rc.UNIQUE_CONSTRAINT_NAME
402
+ AND kcu2.ORDINAL_POSITION = kcu1.ORDINAL_POSITION
403
+ WHERE kcu1.CONSTRAINT_SCHEMA = ? -- COALESCE(current_setting('SEARCH_PATH'), 'public')", schema
404
+ # AND kcu2.TABLE_NAME = ?;", Apartment::Tenant.current, table_name
405
+ ])
406
+ when 'SQLite'
407
+ sql = "SELECT m.name, fkl.\"from\", fkl.\"table\", m.name || '_' || fkl.\"from\" AS constraint_name
408
+ FROM sqlite_master m
409
+ INNER JOIN pragma_foreign_key_list(m.name) fkl ON m.type = 'table'
410
+ ORDER BY m.name, fkl.seq"
411
+ else
412
+ end
413
+ if sql
414
+ ActiveRecord::Base.connection.execute(sql).each do |fk|
415
+ fk = fk.values unless fk.is_a?(Array)
416
+ ::Brick._add_bt_and_hm(fk, relations)
417
+ end
418
+ end
335
419
  end
336
420
 
337
- puts "Classes built from tables:"
421
+ puts "Classes that can be built from tables:"
338
422
  relations.select { |_k, v| !v.key?(:isView) }.keys.each { |k| puts ActiveSupport::Inflector.singularize(k).camelize }
339
- puts "Classes built from views:"
423
+ puts "Classes that can be built from views:"
340
424
  relations.select { |_k, v| v.key?(:isView) }.keys.each { |k| puts ActiveSupport::Inflector.singularize(k).camelize }
341
425
  # pp relations; nil
342
426
 
343
427
  # relations.keys.each { |k| ActiveSupport::Inflector.singularize(k).camelize.constantize }
344
428
  # Layout table describes permissioned hierarchy throughout
345
- x
346
429
  end
347
430
  end
348
431
 
@@ -6,14 +6,32 @@ module Brick
6
6
  class Engine < ::Rails::Engine
7
7
  # paths['app/models'] << 'lib/brick/frameworks/active_record/models'
8
8
  config.brick = ActiveSupport::OrderedOptions.new
9
- initializer 'brick.initialisation' do |app|
10
- Brick.enable_models = app.config.brick.fetch(:enable_models, true)
11
- Brick.enable_controllers = app.config.brick.fetch(:enable_controllers, true)
9
+ ActiveSupport.on_load(:before_initialize) do |app|
10
+ ::Brick.enable_models = app.config.brick.fetch(:enable_models, true)
11
+ ::Brick.enable_controllers = app.config.brick.fetch(:enable_controllers, true)
12
+ ::Brick.enable_views = app.config.brick.fetch(:enable_views, true)
13
+ ::Brick.enable_routes = app.config.brick.fetch(:enable_routes, true)
14
+ ::Brick.skip_database_views = app.config.brick.fetch(:skip_database_views, false)
15
+
16
+ # Specific database tables and views to omit when auto-creating models
17
+ ::Brick.exclude_tables = app.config.brick.fetch(:exclude_tables, [])
18
+
19
+ # Columns to treat as being metadata for purposes of identifying associative tables for has_many :through
20
+ ::Brick.metadata_columns = app.config.brick.fetch(:metadata_columns, ['created_at', 'updated_at', 'deleted_at'])
12
21
 
22
+ # Additional references (virtual foreign keys)
23
+ ::Brick.additional_references = app.config.brick.fetch(:additional_references, nil)
24
+
25
+ # Has one relationships
26
+ ::Brick.has_ones = app.config.brick.fetch(:has_ones, nil)
27
+ end
28
+
29
+ # After we're initialized and before running the rest of stuff, put our configuration in place
30
+ ActiveSupport.on_load(:after_initialize) do
13
31
  # ====================================
14
32
  # Dynamically create generic templates
15
33
  # ====================================
16
- if (Brick.enable_views = app.config.brick.fetch(:enable_views, true))
34
+ if ::Brick.enable_views?
17
35
  ActionView::LookupContext.class_exec do
18
36
  alias :_brick_template_exists? :template_exists?
19
37
  def template_exists?(*args, **options)
@@ -22,12 +40,12 @@ module Brick
22
40
  # args will be something like: ["index", ["categories"]]
23
41
  model = args[1].map(&:camelize).join('::').singularize.constantize
24
42
  if (
25
- is_template_exists = model && (
26
- ['index', 'show'].include?(args.first) || # Everything has index and show
27
- # Only CRU stuff has create / update / destroy
28
- (!model.is_view? && ['new', 'create', 'edit', 'update', 'destroy'].include?(args.first))
29
- )
30
- )
43
+ is_template_exists = model && (
44
+ ['index', 'show'].include?(args.first) || # Everything has index and show
45
+ # Only CRU stuff has create / update / destroy
46
+ (!model.is_view? && ['new', 'create', 'edit', 'update', 'destroy'].include?(args.first))
47
+ )
48
+ )
31
49
  instance_variable_set(:@_brick_model, model)
32
50
  end
33
51
  end
@@ -43,34 +61,21 @@ module Brick
43
61
  table_name = model_name.pluralize.underscore
44
62
  # This gets has_many as well as has_many :through
45
63
  # %%% weed out ones that don't have an available model to reference
46
- bts, hms = @_brick_model.reflect_on_all_associations.each_with_object([{}, {}]) do |a, s|
47
- case a.macro
48
- when :belongs_to
49
- # Build #brick_descrip if needed
50
- unless a.klass.instance_methods(false).include?(:brick_descrip)
51
- descrip_col = (a.klass.columns.map(&:name) - a.klass._brick_get_fks -
52
- (::Brick.config.metadata_columns || []) -
53
- [a.klass.primary_key]).first&.to_sym
54
- if descrip_col
55
- a.klass.define_method :brick_descrip do
56
- send(descrip_col)
57
- end
58
- end
59
- end
60
-
61
- s.first[a.foreign_key] = [a.name, a.klass]
62
- when :has_many
63
- s.last[a.name] = a
64
- end
65
- s
66
- end
64
+ bts, hms = ::Brick.get_bts_and_hms(@_brick_model)
67
65
  # Weed out has_manys that go to an associative table
68
- hms.select { |k, v| v.options[:through] }.each { |_k, hmt| hms.delete(hmt.options[:through]) }
69
- show_obj_blurb = "<td><%= link_to \"#\{#{obj_name}.class.name\} ##\{#{obj_name}.id\}\", #{obj_name} %></td>\n"
70
- hms_headers = hms.each_with_object(+'') { |hm, s| s << "<th>HM #{hm.first}</th>\n" }
66
+ associatives = hms.select { |k, v| v.options[:through] }.each_with_object({}) do |hmt, s|
67
+ s[hmt.first] = hms.delete(hmt.last.options[:through]) # End up with a hash of HMT names pointing to join-table associations
68
+ end
69
+ hms_headers = hms.each_with_object(+'') { |hm, s| s << "<th>HM#{'T' if hm.last.options[:through]} #{hm.first}</th>\n" }
71
70
  hms_columns = hms.each_with_object(+'') do |hm, s|
71
+ hm_fk_name = if hm.last.options[:through]
72
+ associative = associatives[hm.last.name]
73
+ "'#{associative.name}.#{associative.foreign_key}'"
74
+ else
75
+ hm.last.foreign_key
76
+ end
72
77
  s << "<td>
73
- <%= link_to \"#\{#{obj_name}.#{hm.first}.count\} #{hm.first}\", #{hm.last.klass.name.underscore.pluralize}_path({ #{hm.last.foreign_key}: #{obj_name}.#{pk} }) %>
78
+ <%= link_to \"#\{#{obj_name}.#{hm.first}.count\} #{hm.first}\", #{hm.last.klass.name.underscore.pluralize}_path({ #{hm_fk_name}: #{obj_name}.#{pk} }) unless #{obj_name}.#{hm.first}.count.zero? %>
74
79
  </td>\n"
75
80
  end
76
81
 
@@ -94,7 +99,7 @@ module Brick
94
99
  is_need_id_col = true %>
95
100
  </th><th>
96
101
  <% end %>
97
- BT <%= bt[1].name %>
102
+ BT <%= \"#\{bt.first\}-\" unless bt[1].name.underscore == bt.first.to_s %><%= bt[1].name %>
98
103
  <% else
99
104
  is_first = false %>
100
105
  <%= col %>
@@ -104,26 +109,26 @@ module Brick
104
109
  <% if is_first # STILL haven't been able to write a first non-key / non-metadata column?
105
110
  is_first = false
106
111
  is_need_id_col = true %>
107
- <td></td>
112
+ <th></th>
108
113
  <% end %>
109
114
  #{hms_headers}
110
115
  </tr>
111
116
 
112
117
  <% @#{table_name}.each do |#{obj_name}| %>
113
118
  <tr>
114
- <% if is_need_id_col %>
115
- <td>#{show_obj_blurb}</td>
119
+ <% is_first = true
120
+ if is_need_id_col
121
+ is_first = false %>
122
+ <td><%= link_to \"#\{#{obj_name}.class.name\} ##\{#{obj_name}.id\}\", #{obj_name} %></td>
116
123
  <% end %>
117
- <% is_first = true; #{obj_name}.attributes.each do |k, val| %>
124
+ <% #{obj_name}.attributes.each do |k, val| %>
118
125
  <% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) %>
126
+ <td>
119
127
  <% if (bt = bts[k]) %>
120
- <td>
121
128
  <%= obj = bt[1].find_by(bt.last => val); link_to obj.brick_descrip, obj %>
122
129
  <% elsif is_first %>
123
- <td>
124
130
  <%= is_first = false; link_to val, #{obj_name} %>
125
131
  <% else %>
126
- <td>
127
132
  <%= val %>
128
133
  <% end %>
129
134
  </td>
@@ -134,13 +139,11 @@ module Brick
134
139
  <% end %>
135
140
  </table>
136
141
 
137
- <%= link_to \"New #{obj_name}\", new_#{obj_name}_path %>
142
+ #{"<hr><%= link_to \"New #{obj_name}\", new_#{obj_name}_path %>" unless @_brick_model.is_view?}
138
143
  "
139
- # "<%= @#{@_brick_model.name.underscore.pluralize}.inspect %>"
140
144
  when 'show'
141
145
  "<%= @#{@_brick_model.name.underscore}.inspect %>"
142
146
  end
143
- puts inline
144
147
  # As if it were an inline template (see #determine_template in actionview-5.2.6.2/lib/action_view/renderer/template_renderer.rb)
145
148
  keys = options.has_key?(:locals) ? options[:locals].keys : []
146
149
  handler = ActionView::Template.handler_for_extension(options[:type] || 'erb')
@@ -152,7 +155,7 @@ puts inline
152
155
  end
153
156
  end
154
157
 
155
- if (::Brick.enable_routes = app.config.brick.fetch(:enable_routes, true))
158
+ if ::Brick.enable_routes?
156
159
  ActionDispatch::Routing::RouteSet.class_exec do
157
160
  alias _brick_finalize_routeset! finalize!
158
161
  def finalize!(*args, **options)
@@ -161,8 +164,7 @@ puts inline
161
164
  ::Rails.application.routes.append do
162
165
  # %%% TODO: If no auto-controllers then enumerate the controllers folder in order to build matching routes
163
166
  # If auto-controllers and auto-models are both enabled then this makes sense:
164
- relations = (::Brick.instance_variable_get(:@relations) || {})[ActiveRecord::Base.connection_pool.object_id] || {}
165
- relations.each do |k, v|
167
+ ::Brick.relations.each do |k, v|
166
168
  unless existing_controllers.key?(controller_name = k.underscore.pluralize)
167
169
  options = {}
168
170
  options[:only] = [:index, :show] if v.key?(:isView)
@@ -176,20 +178,8 @@ puts inline
176
178
  end
177
179
  end
178
180
 
179
- # Do not consider database views when auto-creating models
180
- ::Brick.skip_database_views = app.config.brick.fetch(:skip_database_views, false)
181
-
182
- # Specific database tables and views to omit when auto-creating models
183
- ::Brick.exclude_tables = app.config.brick.fetch(:exclude_tables, [])
184
-
185
- # Columns to treat as being metadata for purposes of identifying associative tables for has_many :through
186
- ::Brick.metadata_columns = app.config.brick.fetch(:metadata_columns, ['created_at', 'updated_at', 'deleted_at'])
187
-
188
181
  # Additional references (virtual foreign keys)
189
- if (ars = (::Brick.additional_references = app.config.brick.fetch(:additional_references, nil)))
190
- ars = ars.call if ars.is_a?(Proc)
191
- ars = ars.to_a unless ars.is_a?(Array)
192
- ars = [ars] unless ars.empty? || ars.first.is_a?(Array)
182
+ if (ars = ::Brick.config.additional_references)
193
183
  ars.each do |fk|
194
184
  ::Brick._add_bt_and_hm(fk[0..2])
195
185
  end
@@ -201,7 +191,7 @@ puts inline
201
191
  fks = tbl[:fks].each_with_object({}) { |fk, s| s[fk.last[:fk]] = fk.last[:inverse_table] if fk.last[:is_bt]; s }
202
192
  # Aside from the primary key and the metadata columns created_at, updated_at, and deleted_at, if this table only has
203
193
  # foreign keys then it can act as an associative table and thus be used with has_many :through.
204
- if fks.length > 1 && (tbl_cols - fks.keys - (::Brick.config.metadata_columns || []) - tbl[:pkey].values.first).length.zero?
194
+ if fks.length > 1 && (tbl_cols - fks.keys - (::Brick.config.metadata_columns || []) - (tbl[:pkey].values.first || [])).length.zero?
205
195
  fks.each { |fk| tbl[:hmt_fks][fk.first] = fk.last }
206
196
  end
207
197
  end
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 2
8
+ TINY = 5
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
@@ -76,7 +76,10 @@ end
76
76
  # is first established), and then automatically creates models, controllers, views,
77
77
  # and routes based on those available relations.
78
78
  require 'brick/config'
79
- require 'brick/frameworks/rails'
79
+ if Gem::Specification.all_names.any? { |g| g.start_with?('rails-') }
80
+ require 'rails'
81
+ require 'brick/frameworks/rails'
82
+ end
80
83
  module Brick
81
84
  class << self
82
85
  # All tables and views (what Postgres calls "relations" including column and foreign key info)
@@ -87,6 +90,30 @@ module Brick
87
90
  (connections[ActiveRecord::Base.connection_pool.object_id] ||= Hash.new { |h, k| h[k] = Hash.new { |h, k| h[k] = {} } })
88
91
  end
89
92
 
93
+ def get_bts_and_hms(model)
94
+ model.reflect_on_all_associations.each_with_object([{}, {}]) do |a, s|
95
+ case a.macro
96
+ when :belongs_to
97
+ # Build #brick_descrip if needed
98
+ if a.klass.instance_methods(false).exclude?(:brick_descrip)
99
+ descrip_col = (a.klass.columns.map(&:name) - a.klass._brick_get_fks -
100
+ (::Brick.config.metadata_columns || []) -
101
+ [a.klass.primary_key]).first&.to_sym
102
+ if descrip_col
103
+ a.klass.define_method :brick_descrip do
104
+ send(descrip_col)
105
+ end
106
+ end
107
+ end
108
+
109
+ s.first[a.foreign_key] = [a.name, a.klass]
110
+ when :has_many
111
+ s.last[a.name] = a
112
+ end
113
+ s
114
+ end
115
+ end
116
+
90
117
  # Switches Brick auto-models on or off, for all threads
91
118
  # @api public
92
119
  def enable_models=(value)
@@ -156,8 +183,28 @@ module Brick
156
183
 
157
184
  # Additional table associations to use (Think of these as virtual foreign keys perhaps)
158
185
  # @api public
159
- def additional_references=(value)
160
- Brick.config.additional_references = value
186
+ def additional_references=(ars)
187
+ if ars
188
+ ars = ars.call if ars.is_a?(Proc)
189
+ ars = ars.to_a unless ars.is_a?(Array)
190
+ ars = [ars] unless ars.empty? || ars.first.is_a?(Array)
191
+ Brick.config.additional_references = ars
192
+ end
193
+ end
194
+
195
+ # Associations to treat has a has_one
196
+ # @api public
197
+ def has_ones=(hos)
198
+ if hos
199
+ hos = hos.call if hos.is_a?(Proc)
200
+ hos = hos.to_a unless hos.is_a?(Array)
201
+ hos = [hos] unless hos.empty? || hos.first.is_a?(Array)
202
+ # Translate to being nested hashes
203
+ Brick.config.has_ones = hos&.each_with_object(Hash.new { |h, k| h[k] = {} }) do |v, s|
204
+ s[v.first][v[1]] = v[2] if v[1]
205
+ s
206
+ end
207
+ end
161
208
  end
162
209
 
163
210
 
@@ -197,10 +244,7 @@ module Brick
197
244
  end
198
245
  end
199
246
 
200
- require 'brick/extensions'
201
247
  require 'brick/version_number'
202
- # require 'brick/serializers/json'
203
- # require 'brick/serializers/yaml'
204
248
 
205
249
  require 'active_record'
206
250
  # Major compatibility fixes for ActiveRecord < 4.2
@@ -426,18 +470,18 @@ ActiveSupport.on_load(:active_record) do
426
470
  end
427
471
  end
428
472
 
429
- include ::Brick::Extensions
430
-
431
- unless ::Brick::Extensions::IS_AMOEBA
432
- # Add amoeba-compatible support
433
- module ActiveRecord
434
- class Base
435
- def self.amoeba(*args)
436
- puts "Amoeba called from #{name} with #{args.inspect}"
437
- end
438
- end
439
- end
440
- end
473
+ # include ::Brick::Extensions
474
+
475
+ # unless ::Brick::Extensions::IS_AMOEBA
476
+ # # Add amoeba-compatible support
477
+ # module ActiveRecord
478
+ # class Base
479
+ # def self.amoeba(*args)
480
+ # puts "Amoeba called from #{name} with #{args.inspect}"
481
+ # end
482
+ # end
483
+ # end
484
+ # end
441
485
  end
442
486
 
443
487
  # Do this earlier because stuff here gets mixed into JoinDependency::JoinAssociation and AssociationScope
@@ -526,3 +570,5 @@ if ActiveRecord.version < ::Gem::Version.new('5.2')
526
570
  end # module ActiveRecord
527
571
  # rubocop:enable Style/CommentedKeyword
528
572
  end
573
+
574
+ require 'brick/extensions'
@@ -48,6 +48,14 @@ module Brick
48
48
  # Brick.additional_references = [['orders', 'customer_id', 'customer'],
49
49
  # ['customer', 'region_id', 'regions']]
50
50
 
51
+ # # By default primary tables involved in a foreign key relationship will indicate a \"has_many\" relationship pointing
52
+ # # back to the foreign table. In order to represent a \"has_one\" association instead, an override can be provided
53
+ # # using the primary model name and the association name which you instead want to have treated as a \"has_one\":
54
+ # Brick.has_ones = [['User', 'user_profile']]
55
+ # # If you want to use an alternate name for the \"has_one\", such as in the case above calling the association \"profile\"
56
+ # # instead of \"user_profile\", then apply that as a third parameter like this:
57
+ # Brick.has_ones = [['User', 'user_profile', 'profile']]
58
+
51
59
  # # We normally don't consider the timestamp columns \"created_at\", \"updated_at\", and \"deleted_at\" to count when
52
60
  # # finding tables which can serve as associative tables in an N:M association. That is, ones that can be a
53
61
  # # part of a has_many :through association. If you want to use different exclusion columns than our defaults
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.2
4
+ version: 1.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lorin Thwaits
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-03-13 00:00:00.000000000 Z
11
+ date: 2022-03-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -156,6 +156,20 @@ dependencies:
156
156
  - - "~>"
157
157
  - !ruby/object:Gem::Version
158
158
  version: 1.42.0
159
+ - !ruby/object:Gem::Dependency
160
+ name: mysql2
161
+ requirement: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - "~>"
164
+ - !ruby/object:Gem::Version
165
+ version: '0.5'
166
+ type: :development
167
+ prerelease: false
168
+ version_requirements: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - "~>"
171
+ - !ruby/object:Gem::Version
172
+ version: '0.5'
159
173
  - !ruby/object:Gem::Dependency
160
174
  name: pg
161
175
  requirement: !ruby/object:Gem::Requirement