brick 1.0.3 → 1.0.6
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 +4 -4
- data/lib/brick/config.rb +9 -0
- data/lib/brick/extensions.rb +156 -74
- data/lib/brick/frameworks/rails/engine.rb +103 -123
- data/lib/brick/version_number.rb +1 -1
- data/lib/brick.rb +68 -18
- data/lib/generators/brick/install_generator.rb +8 -0
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 634669de2074746d62642b8c9108070b78e92b14f7924ac969404b240de2267d
|
4
|
+
data.tar.gz: 7614fa456fcaeb6bc68227230088fe3adeed31278241a000f3fb19792b763d26
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3b89ed9c8ffe3c63e098a3f7a8e5a9987db0c00bfe2c917b7c2acefebc30653a4a88d091852b8f8e6128daa310e7c7742d10611acfb0ffe7d06cc6c01da7068e
|
7
|
+
data.tar.gz: 98d58b4461b9e85537da640e3940316553d142a341547086ce614f75d9366145c8818beb6e972ac93a77e7209b034eb64a64eb9a078f6afb91fbbd008e3ade69
|
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
|
data/lib/brick/extensions.rb
CHANGED
@@ -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 =
|
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
|
@@ -57,14 +61,15 @@ module ActiveRecord
|
|
57
61
|
wheres = {}
|
58
62
|
rel_joins = []
|
59
63
|
params.each do |k, v|
|
60
|
-
|
64
|
+
case (ks = k.split('.')).length
|
65
|
+
when 1
|
66
|
+
next unless klass._brick_get_fks.include?(k)
|
67
|
+
when 2
|
61
68
|
assoc_name = ks.first.to_sym
|
62
69
|
# Make sure it's a good association name and that the model has that column name
|
63
70
|
next unless klass.reflect_on_association(assoc_name)&.klass&.columns&.map(&:name)&.include?(ks.last)
|
64
71
|
|
65
72
|
rel_joins << assoc_name unless rel_joins.include?(assoc_name)
|
66
|
-
else
|
67
|
-
next unless klass._brick_get_fks.include?(k)
|
68
73
|
end
|
69
74
|
wheres[k] = v.split(',')
|
70
75
|
end
|
@@ -75,17 +80,19 @@ module ActiveRecord
|
|
75
80
|
end
|
76
81
|
end
|
77
82
|
end
|
78
|
-
end
|
79
83
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
84
|
+
module Inheritance
|
85
|
+
module ClassMethods
|
86
|
+
private
|
87
|
+
|
88
|
+
alias _brick_find_sti_class find_sti_class
|
89
|
+
def find_sti_class(type_name)
|
90
|
+
::Brick.sti_models[type_name] = { base: self } unless type_name.blank?
|
91
|
+
_brick_find_sti_class(type_name)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
89
96
|
|
90
97
|
# Object.class_exec do
|
91
98
|
class Object
|
@@ -118,6 +125,11 @@ class Object
|
|
118
125
|
singular_table_name = ActiveSupport::Inflector.underscore(model_name)
|
119
126
|
table_name = ActiveSupport::Inflector.pluralize(singular_table_name)
|
120
127
|
|
128
|
+
# Adjust for STI if we know of a base model for the requested model name
|
129
|
+
if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, nil))
|
130
|
+
table_name = base_model.table_name
|
131
|
+
end
|
132
|
+
|
121
133
|
# Maybe, just maybe there's a database table that will satisfy this need
|
122
134
|
if (matching = [table_name, singular_table_name, plural_class_name, model_name].find { |m| relations.key?(m) })
|
123
135
|
build_model(model_name, singular_table_name, table_name, relations, matching)
|
@@ -143,8 +155,9 @@ class Object
|
|
143
155
|
if table_name == singular_table_name && !ActiveSupport::Inflector.inflections.uncountable.include?(table_name)
|
144
156
|
raise NameError.new("Class name for a model that references table \"#{matching}\" should be \"#{ActiveSupport::Inflector.singularize(model_name)}\".")
|
145
157
|
end
|
146
|
-
|
147
|
-
|
158
|
+
base_model = ::Brick.sti_models[model_name]&.fetch(:base, nil) || ActiveRecord::Base
|
159
|
+
code = +"class #{model_name} < #{base_model.name}\n"
|
160
|
+
built_model = Class.new(base_model) do |new_model_class|
|
148
161
|
Object.const_set(model_name.to_sym, new_model_class)
|
149
162
|
# Accommodate singular or camel-cased table names such as "order_detail" or "OrderDetails"
|
150
163
|
code << " self.table_name = '#{self.table_name = matching}'\n" unless table_name == matching
|
@@ -180,15 +193,6 @@ class Object
|
|
180
193
|
code << " # Could not identify any column(s) to use as a primary key\n" unless is_view
|
181
194
|
end
|
182
195
|
|
183
|
-
# if relation[:cols].key?('last_update')
|
184
|
-
# define_method :updated_at do
|
185
|
-
# last_update
|
186
|
-
# end
|
187
|
-
# define_method :'updated_at=' do |val|
|
188
|
-
# last_update=(val)
|
189
|
-
# end
|
190
|
-
# end
|
191
|
-
|
192
196
|
fks = relation[:fks] || {}
|
193
197
|
fks.each do |_constraint_name, assoc|
|
194
198
|
assoc_name = assoc[:assoc_name]
|
@@ -199,21 +203,38 @@ class Object
|
|
199
203
|
need_class_name = singular_table_name.underscore != assoc_name
|
200
204
|
need_fk = "#{assoc_name}_id" != assoc[:fk]
|
201
205
|
inverse_assoc_name, _x = _brick_get_hm_assoc_name(relations[assoc[:inverse_table]], assoc[:inverse])
|
206
|
+
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))
|
207
|
+
inverse_assoc_name = if has_ones[singular_inv_assoc_name]
|
208
|
+
need_inverse_of = true
|
209
|
+
has_ones[singular_inv_assoc_name]
|
210
|
+
else
|
211
|
+
singular_inv_assoc_name
|
212
|
+
end
|
213
|
+
end
|
202
214
|
:belongs_to
|
203
215
|
else
|
204
216
|
# need_class_name = ActiveSupport::Inflector.singularize(assoc_name) == ActiveSupport::Inflector.singularize(table_name.underscore)
|
205
217
|
# Are there multiple foreign keys out to the same table?
|
206
218
|
assoc_name, need_class_name = _brick_get_hm_assoc_name(relation, assoc)
|
207
|
-
need_fk = "#{
|
219
|
+
need_fk = "#{ActiveSupport::Inflector.singularize(assoc[:inverse][:inverse_table])}_id" != assoc[:fk]
|
208
220
|
# fks[table_name].find { |other_assoc| other_assoc.object_id != assoc.object_id && other_assoc[:assoc_name] == assoc[assoc_name] }
|
209
|
-
|
221
|
+
if (has_ones = ::Brick.config.has_ones&.fetch(model_name, nil))&.key?(singular_assoc_name = ActiveSupport::Inflector.singularize(assoc_name))
|
222
|
+
assoc_name = if has_ones[singular_assoc_name]
|
223
|
+
need_class_name = true
|
224
|
+
has_ones[singular_assoc_name]
|
225
|
+
else
|
226
|
+
singular_assoc_name
|
227
|
+
end
|
228
|
+
:has_one
|
229
|
+
else
|
230
|
+
:has_many
|
231
|
+
end
|
210
232
|
end
|
233
|
+
# Figure out if we need to specially call out the class_name and/or foreign key
|
234
|
+
# (and if either of those then definitely also a specific inverse_of)
|
211
235
|
options[:class_name] = singular_table_name.camelize if need_class_name
|
212
|
-
|
213
|
-
if
|
214
|
-
options[:foreign_key] = assoc[:fk].to_sym
|
215
|
-
end
|
216
|
-
options[:inverse_of] = inverse_assoc_name.to_sym if need_class_name || need_fk
|
236
|
+
options[:foreign_key] = assoc[:fk].to_sym if need_fk # Funky foreign key?
|
237
|
+
options[:inverse_of] = inverse_assoc_name.to_sym if need_class_name || need_fk || need_inverse_of
|
217
238
|
assoc_name = assoc_name.to_sym
|
218
239
|
code << " #{macro} #{assoc_name.inspect}#{options.map { |k, v| ", #{k}: #{v.inspect}" }.join}\n"
|
219
240
|
self.send(macro, assoc_name, **options)
|
@@ -243,7 +264,7 @@ class Object
|
|
243
264
|
Object.const_set(class_name.to_sym, new_controller_class)
|
244
265
|
|
245
266
|
code << " def index\n"
|
246
|
-
code << " @#{table_name} = #{model.name}#{model.primary_key ? ".order(#{model.primary_key.inspect}" : '.all'}
|
267
|
+
code << " @#{table_name} = #{model.name}#{model.primary_key ? ".order(#{model.primary_key.inspect})" : '.all'}\n"
|
247
268
|
code << " @#{table_name}.brick_where(params)\n"
|
248
269
|
code << " end\n"
|
249
270
|
self.define_method :index do
|
@@ -288,16 +309,38 @@ end
|
|
288
309
|
module ActiveRecord::ConnectionHandling
|
289
310
|
alias _brick_establish_connection establish_connection
|
290
311
|
def establish_connection(*args)
|
291
|
-
|
312
|
+
conn = _brick_establish_connection(*args)
|
313
|
+
_brick_reflect_tables
|
314
|
+
conn
|
315
|
+
end
|
292
316
|
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
317
|
+
def _brick_reflect_tables
|
318
|
+
if (relations = ::Brick.relations).empty?
|
319
|
+
# Only for Postgres? (Doesn't work in sqlite3)
|
320
|
+
# puts ActiveRecord::Base.connection.execute("SELECT current_setting('SEARCH_PATH')").to_a.inspect
|
321
|
+
|
322
|
+
case ActiveRecord::Base.connection.adapter_name
|
323
|
+
when 'PostgreSQL'
|
324
|
+
schema = 'public'
|
325
|
+
when 'Mysql2'
|
326
|
+
schema = ActiveRecord::Base.connection.current_database
|
327
|
+
when 'SQLite'
|
328
|
+
sql = "SELECT m.name AS relation_name, UPPER(m.type) AS table_type,
|
329
|
+
p.name AS column_name, p.type AS data_type,
|
330
|
+
CASE p.pk WHEN 1 THEN 'PRIMARY KEY' END AS const
|
331
|
+
FROM sqlite_master AS m
|
332
|
+
INNER JOIN pragma_table_info(m.name) AS p
|
333
|
+
WHERE m.name NOT IN ('ar_internal_metadata', 'schema_migrations')
|
334
|
+
ORDER BY m.name, p.cid"
|
335
|
+
else
|
336
|
+
puts "Unfamiliar with connection adapter #{ActiveRecord::Base.connection.adapter_name}"
|
337
|
+
end
|
338
|
+
|
339
|
+
sql ||= ActiveRecord::Base.send(:sanitize_sql_array, [
|
297
340
|
"SELECT t.table_name AS relation_name, t.table_type,
|
298
341
|
c.column_name, c.data_type,
|
299
342
|
COALESCE(c.character_maximum_length, c.numeric_precision) AS max_length,
|
300
|
-
tc.constraint_type AS const, kcu.constraint_name AS key
|
343
|
+
tc.constraint_type AS const, kcu.constraint_name AS \"key\"
|
301
344
|
FROM INFORMATION_SCHEMA.tables AS t
|
302
345
|
LEFT OUTER JOIN INFORMATION_SCHEMA.columns AS c ON t.table_schema = c.table_schema
|
303
346
|
AND t.table_name = c.table_name
|
@@ -309,48 +352,88 @@ module ActiveRecord::ConnectionHandling
|
|
309
352
|
AND kcu.ordinal_position = c.ordinal_position
|
310
353
|
LEFT OUTER JOIN INFORMATION_SCHEMA.table_constraints AS tc
|
311
354
|
ON kcu.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA
|
355
|
+
AND kcu.TABLE_NAME = tc.TABLE_NAME
|
312
356
|
AND kcu.CONSTRAINT_NAME = tc.constraint_name
|
313
357
|
WHERE t.table_schema = ? -- COALESCE(current_setting('SEARCH_PATH'), 'public')
|
314
358
|
-- AND t.table_type IN ('VIEW') -- 'BASE TABLE', 'FOREIGN TABLE'
|
315
359
|
AND t.table_name NOT IN ('pg_stat_statements', 'ar_internal_metadata', 'schema_migrations')
|
316
360
|
ORDER BY 1, t.table_type DESC, c.ordinal_position", schema
|
317
361
|
])
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
362
|
+
|
363
|
+
measures = []
|
364
|
+
case ActiveRecord::Base.connection.adapter_name
|
365
|
+
when 'PostgreSQL', 'SQLite' # These bring back a hash for each row because the query uses column aliases
|
366
|
+
ActiveRecord::Base.connection.execute(sql).each do |r|
|
367
|
+
# next if internal_views.include?(r['relation_name']) # Skip internal views such as v_all_assessments
|
368
|
+
relation = relations[(relation_name = r['relation_name'])]
|
369
|
+
relation[:isView] = true if r['table_type'] == 'VIEW'
|
370
|
+
col_name = r['column_name']
|
371
|
+
key = case r['const']
|
372
|
+
when 'PRIMARY KEY'
|
373
|
+
relation[:pkey][r['key'] || relation_name] ||= []
|
374
|
+
when 'UNIQUE'
|
375
|
+
relation[:ukeys][r['key'] || "#{relation_name}.#{col_name}"] ||= []
|
376
|
+
# key = (relation[:ukeys] = Hash.new { |h, k| h[k] = [] }) if key.is_a?(Array)
|
377
|
+
# key[r['key']]
|
378
|
+
end
|
379
|
+
key << col_name if key
|
380
|
+
cols = relation[:cols] # relation.fetch(:cols) { relation[:cols] = [] }
|
381
|
+
cols[col_name] = [r['data_type'], r['max_length'], measures&.include?(col_name)]
|
382
|
+
# puts "KEY! #{r['relation_name']}.#{col_name} #{r['key']} #{r['const']}" if r['key']
|
383
|
+
end
|
384
|
+
else # MySQL2 acts a little differently, bringing back an array for each row
|
385
|
+
ActiveRecord::Base.connection.execute(sql).each do |r|
|
386
|
+
# next if internal_views.include?(r['relation_name']) # Skip internal views such as v_all_assessments
|
387
|
+
relation = relations[(relation_name = r[0])] # here relation represents a table or view from the database
|
388
|
+
relation[:isView] = true if r[1] == 'VIEW' # table_type
|
389
|
+
col_name = r[2]
|
390
|
+
key = case r[5] # constraint type
|
391
|
+
when 'PRIMARY KEY'
|
392
|
+
# key
|
393
|
+
relation[:pkey][r[6] || relation_name] ||= []
|
394
|
+
when 'UNIQUE'
|
395
|
+
relation[:ukeys][r[6] || "#{relation_name}.#{col_name}"] ||= []
|
396
|
+
# key = (relation[:ukeys] = Hash.new { |h, k| h[k] = [] }) if key.is_a?(Array)
|
397
|
+
# key[r['key']]
|
398
|
+
end
|
399
|
+
key << col_name if key
|
400
|
+
cols = relation[:cols] # relation.fetch(:cols) { relation[:cols] = [] }
|
401
|
+
# 'data_type', 'max_length'
|
402
|
+
cols[col_name] = [r[3], r[4], measures&.include?(col_name)]
|
403
|
+
# puts "KEY! #{r['relation_name']}.#{col_name} #{r['key']} #{r['const']}" if r['key']
|
404
|
+
end
|
336
405
|
end
|
337
406
|
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
407
|
+
case ActiveRecord::Base.connection.adapter_name
|
408
|
+
when 'PostgreSQL', 'Mysql2'
|
409
|
+
sql = ActiveRecord::Base.send(:sanitize_sql_array, [
|
410
|
+
"SELECT kcu1.TABLE_NAME, kcu1.COLUMN_NAME, kcu2.TABLE_NAME AS primary_table, kcu1.CONSTRAINT_NAME
|
411
|
+
FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS rc
|
412
|
+
INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu1
|
413
|
+
ON kcu1.CONSTRAINT_CATALOG = rc.CONSTRAINT_CATALOG
|
414
|
+
AND kcu1.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA
|
415
|
+
AND kcu1.CONSTRAINT_NAME = rc.CONSTRAINT_NAME
|
416
|
+
INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu2
|
417
|
+
ON kcu2.CONSTRAINT_CATALOG = rc.UNIQUE_CONSTRAINT_CATALOG
|
418
|
+
AND kcu2.CONSTRAINT_SCHEMA = rc.UNIQUE_CONSTRAINT_SCHEMA
|
419
|
+
AND kcu2.CONSTRAINT_NAME = rc.UNIQUE_CONSTRAINT_NAME
|
420
|
+
AND kcu2.ORDINAL_POSITION = kcu1.ORDINAL_POSITION
|
421
|
+
WHERE kcu1.CONSTRAINT_SCHEMA = ? -- COALESCE(current_setting('SEARCH_PATH'), 'public')", schema
|
422
|
+
# AND kcu2.TABLE_NAME = ?;", Apartment::Tenant.current, table_name
|
423
|
+
])
|
424
|
+
when 'SQLite'
|
425
|
+
sql = "SELECT m.name, fkl.\"from\", fkl.\"table\", m.name || '_' || fkl.\"from\" AS constraint_name
|
426
|
+
FROM sqlite_master m
|
427
|
+
INNER JOIN pragma_foreign_key_list(m.name) fkl ON m.type = 'table'
|
428
|
+
ORDER BY m.name, fkl.seq"
|
429
|
+
else
|
430
|
+
end
|
431
|
+
if sql
|
432
|
+
ActiveRecord::Base.connection.execute(sql).each do |fk|
|
433
|
+
fk = fk.values unless fk.is_a?(Array)
|
434
|
+
::Brick._add_bt_and_hm(fk, relations)
|
435
|
+
end
|
436
|
+
end
|
354
437
|
end
|
355
438
|
|
356
439
|
puts "Classes that can be built from tables:"
|
@@ -361,7 +444,6 @@ module ActiveRecord::ConnectionHandling
|
|
361
444
|
|
362
445
|
# relations.keys.each { |k| ActiveSupport::Inflector.singularize(k).camelize.constantize }
|
363
446
|
# Layout table describes permissioned hierarchy throughout
|
364
|
-
x
|
365
447
|
end
|
366
448
|
end
|
367
449
|
|
@@ -5,11 +5,8 @@ module Brick
|
|
5
5
|
# See http://guides.rubyonrails.org/engines.html
|
6
6
|
class Engine < ::Rails::Engine
|
7
7
|
# paths['app/models'] << 'lib/brick/frameworks/active_record/models'
|
8
|
-
puts "BEFORE - engine set config"
|
9
8
|
config.brick = ActiveSupport::OrderedOptions.new
|
10
|
-
# initializer 'brick.initialisation' do |app|
|
11
9
|
ActiveSupport.on_load(:before_initialize) do |app|
|
12
|
-
puts "BEFORE - engine initialisation"
|
13
10
|
::Brick.enable_models = app.config.brick.fetch(:enable_models, true)
|
14
11
|
::Brick.enable_controllers = app.config.brick.fetch(:enable_controllers, true)
|
15
12
|
::Brick.enable_views = app.config.brick.fetch(:enable_views, true)
|
@@ -25,83 +22,72 @@ module Brick
|
|
25
22
|
# Additional references (virtual foreign keys)
|
26
23
|
::Brick.additional_references = app.config.brick.fetch(:additional_references, nil)
|
27
24
|
|
28
|
-
#
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
31
|
+
# ====================================
|
32
|
+
# Dynamically create generic templates
|
33
|
+
# ====================================
|
34
|
+
if ::Brick.enable_views?
|
35
|
+
ActionView::LookupContext.class_exec do
|
36
|
+
alias :_brick_template_exists? :template_exists?
|
37
|
+
def template_exists?(*args, **options)
|
38
|
+
unless (is_template_exists = _brick_template_exists?(*args, **options))
|
39
|
+
# Need to return true if we can fill in the blanks for a missing one
|
40
|
+
# args will be something like: ["index", ["categories"]]
|
41
|
+
model = args[1].map(&:camelize).join('::').singularize.constantize
|
42
|
+
if (
|
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
|
+
)
|
49
|
+
instance_variable_set(:@_brick_model, model)
|
51
50
|
end
|
52
|
-
is_template_exists
|
53
51
|
end
|
52
|
+
is_template_exists
|
53
|
+
end
|
54
54
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
s.first[a.foreign_key] = [a.name, a.klass]
|
80
|
-
when :has_many
|
81
|
-
s.last[a.name] = a
|
82
|
-
end
|
83
|
-
s
|
84
|
-
end
|
85
|
-
# Weed out has_manys that go to an associative table
|
86
|
-
associatives = hms.select { |k, v| v.options[:through] }.each_with_object({}) do |hmt, s|
|
87
|
-
s[hmt.first] = hms.delete(hmt.last.options[:through]) # End up with a hash of HMT names pointing to join-table associations
|
55
|
+
alias :_brick_find_template :find_template
|
56
|
+
def find_template(*args, **options)
|
57
|
+
if @_brick_model
|
58
|
+
model_name = @_brick_model.name
|
59
|
+
pk = @_brick_model.primary_key
|
60
|
+
obj_name = model_name.underscore
|
61
|
+
table_name = model_name.pluralize.underscore
|
62
|
+
# This gets has_many as well as has_many :through
|
63
|
+
# %%% weed out ones that don't have an available model to reference
|
64
|
+
bts, hms = ::Brick.get_bts_and_hms(@_brick_model)
|
65
|
+
# Weed out has_manys that go to an associative table
|
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>H#{hm.last.macro == :has_one ? 'O' : 'M'}#{'T' if hm.last.options[:through]} #{hm.first}</th>\n" }
|
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
|
88
76
|
end
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
associative = associatives[hm.last.name]
|
93
|
-
"'#{associative.name}.#{associative.foreign_key}'"
|
94
|
-
else
|
95
|
-
hm.last.foreign_key
|
96
|
-
end
|
97
|
-
s << "<td>
|
98
|
-
<%= link_to \"#\{#{obj_name}.#{hm.first}.count\} #{hm.first}\", #{hm.last.klass.name.underscore.pluralize}_path({ #{hm_fk_name}: #{obj_name}.#{pk} }) %>
|
77
|
+
s << if hm.last.macro == :has_many
|
78
|
+
"<td>
|
79
|
+
<%= 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? %>
|
99
80
|
</td>\n"
|
100
|
-
|
81
|
+
else # has_one
|
82
|
+
"<td>
|
83
|
+
<%= obj = #{obj_name}.#{hm.first}; link_to(obj.brick_descrip, obj) if obj %>
|
84
|
+
</td>\n"
|
85
|
+
end
|
86
|
+
end
|
101
87
|
|
102
|
-
|
103
|
-
|
104
|
-
|
88
|
+
inline = case args.first
|
89
|
+
when 'index'
|
90
|
+
"<p style=\"color: green\"><%= notice %></p>
|
105
91
|
|
106
92
|
<h1>#{model_name.pluralize}</h1>
|
107
93
|
<% if @_brick_params&.present? %><h3>where <%= @_brick_params.each_with_object([]) { |v, s| s << \"#\{v.first\} = #\{v.last.inspect\}\" }.join(', ') %></h3><% end %>
|
@@ -109,7 +95,7 @@ module Brick
|
|
109
95
|
<table id=\"#{table_name}\">
|
110
96
|
<tr>
|
111
97
|
<% is_first = true; is_need_id_col = nil
|
112
|
-
bts = { #{bts.each_with_object([]) { |v, s| s << "#{v.first.inspect} => [#{v.last.first.inspect}, #{v.last.
|
98
|
+
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(', ')} }
|
113
99
|
@#{table_name}.columns.map(&:name).each do |col| %>
|
114
100
|
<% next if col == '#{pk}' || ::Brick.config.metadata_columns.include?(col) %>
|
115
101
|
<th>
|
@@ -119,7 +105,7 @@ module Brick
|
|
119
105
|
is_need_id_col = true %>
|
120
106
|
</th><th>
|
121
107
|
<% end %>
|
122
|
-
BT <%= bt[1].name %>
|
108
|
+
BT <%= \"#\{bt.first\}-\" unless bt[1].name.underscore == bt.first.to_s %><%= bt[1].name %>
|
123
109
|
<% else
|
124
110
|
is_first = false %>
|
125
111
|
<%= col %>
|
@@ -145,9 +131,9 @@ module Brick
|
|
145
131
|
<% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) %>
|
146
132
|
<td>
|
147
133
|
<% if (bt = bts[k]) %>
|
148
|
-
<%= obj = bt[1].find_by(bt.last => val); link_to
|
134
|
+
<%= obj = bt[1].find_by(bt.last => val); link_to(obj.brick_descrip, obj) if obj %>
|
149
135
|
<% elsif is_first %>
|
150
|
-
<%= is_first = false; link_to val, #{obj_name} %>
|
136
|
+
<%= is_first = false; link_to val, #{obj_name}_path(#{obj_name}.#{pk}) %>
|
151
137
|
<% else %>
|
152
138
|
<%= val %>
|
153
139
|
<% end %>
|
@@ -159,66 +145,60 @@ module Brick
|
|
159
145
|
<% end %>
|
160
146
|
</table>
|
161
147
|
|
162
|
-
|
148
|
+
#{"<hr><%= link_to \"New #{obj_name}\", new_#{obj_name}_path %>" unless @_brick_model.is_view?}
|
163
149
|
"
|
164
|
-
|
165
|
-
|
166
|
-
"<%= @#{@_brick_model.name.underscore}.inspect %>"
|
167
|
-
end
|
168
|
-
# As if it were an inline template (see #determine_template in actionview-5.2.6.2/lib/action_view/renderer/template_renderer.rb)
|
169
|
-
keys = options.has_key?(:locals) ? options[:locals].keys : []
|
170
|
-
handler = ActionView::Template.handler_for_extension(options[:type] || 'erb')
|
171
|
-
ActionView::Template.new(inline, "auto-generated #{args.first} template", handler, locals: keys)
|
172
|
-
else
|
173
|
-
_brick_find_template(*args, **options)
|
150
|
+
when 'show'
|
151
|
+
"<%= @#{@_brick_model.name.underscore}.inspect %>"
|
174
152
|
end
|
153
|
+
# As if it were an inline template (see #determine_template in actionview-5.2.6.2/lib/action_view/renderer/template_renderer.rb)
|
154
|
+
keys = options.has_key?(:locals) ? options[:locals].keys : []
|
155
|
+
handler = ActionView::Template.handler_for_extension(options[:type] || 'erb')
|
156
|
+
ActionView::Template.new(inline, "auto-generated #{args.first} template", handler, locals: keys)
|
157
|
+
else
|
158
|
+
_brick_find_template(*args, **options)
|
175
159
|
end
|
176
160
|
end
|
177
161
|
end
|
162
|
+
end
|
178
163
|
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
send(:resources, controller_name.to_sym, **options)
|
194
|
-
end
|
164
|
+
if ::Brick.enable_routes?
|
165
|
+
ActionDispatch::Routing::RouteSet.class_exec do
|
166
|
+
alias _brick_finalize_routeset! finalize!
|
167
|
+
def finalize!(*args, **options)
|
168
|
+
unless @finalized
|
169
|
+
existing_controllers = routes.each_with_object({}) { |r, s| c = r.defaults[:controller]; s[c] = nil if c }
|
170
|
+
::Rails.application.routes.append do
|
171
|
+
# %%% TODO: If no auto-controllers then enumerate the controllers folder in order to build matching routes
|
172
|
+
# If auto-controllers and auto-models are both enabled then this makes sense:
|
173
|
+
::Brick.relations.each do |k, v|
|
174
|
+
unless existing_controllers.key?(controller_name = k.underscore.pluralize)
|
175
|
+
options = {}
|
176
|
+
options[:only] = [:index, :show] if v.key?(:isView)
|
177
|
+
send(:resources, controller_name.to_sym, **options)
|
195
178
|
end
|
196
179
|
end
|
197
180
|
end
|
198
|
-
_brick_finalize_routeset!(*args, **options)
|
199
181
|
end
|
182
|
+
_brick_finalize_routeset!(*args, **options)
|
200
183
|
end
|
201
184
|
end
|
185
|
+
end
|
202
186
|
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
ars = [ars] unless ars.empty? || ars.first.is_a?(Array)
|
208
|
-
ars.each do |fk|
|
209
|
-
::Brick._add_bt_and_hm(fk[0..2])
|
210
|
-
end
|
187
|
+
# Additional references (virtual foreign keys)
|
188
|
+
if (ars = ::Brick.config.additional_references)
|
189
|
+
ars.each do |fk|
|
190
|
+
::Brick._add_bt_and_hm(fk[0..2])
|
211
191
|
end
|
192
|
+
end
|
212
193
|
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
end
|
194
|
+
# Find associative tables that can be set up for has_many :through
|
195
|
+
::Brick.relations.each do |_key, tbl|
|
196
|
+
tbl_cols = tbl[:cols].keys
|
197
|
+
fks = tbl[:fks].each_with_object({}) { |fk, s| s[fk.last[:fk]] = fk.last[:inverse_table] if fk.last[:is_bt]; s }
|
198
|
+
# Aside from the primary key and the metadata columns created_at, updated_at, and deleted_at, if this table only has
|
199
|
+
# foreign keys then it can act as an associative table and thus be used with has_many :through.
|
200
|
+
if fks.length > 1 && (tbl_cols - fks.keys - (::Brick.config.metadata_columns || []) - (tbl[:pkey].values.first || [])).length.zero?
|
201
|
+
fks.each { |fk| tbl[:hmt_fks][fk.first] = fk.last }
|
222
202
|
end
|
223
203
|
end
|
224
204
|
end
|
data/lib/brick/version_number.rb
CHANGED
data/lib/brick.rb
CHANGED
@@ -76,8 +76,15 @@ 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
|
-
|
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
|
84
|
+
def self.sti_models
|
85
|
+
@sti_models ||= {}
|
86
|
+
end
|
87
|
+
|
81
88
|
class << self
|
82
89
|
# All tables and views (what Postgres calls "relations" including column and foreign key info)
|
83
90
|
def relations
|
@@ -87,6 +94,30 @@ module Brick
|
|
87
94
|
(connections[ActiveRecord::Base.connection_pool.object_id] ||= Hash.new { |h, k| h[k] = Hash.new { |h, k| h[k] = {} } })
|
88
95
|
end
|
89
96
|
|
97
|
+
def get_bts_and_hms(model)
|
98
|
+
model.reflect_on_all_associations.each_with_object([{}, {}]) do |a, s|
|
99
|
+
case a.macro
|
100
|
+
when :belongs_to
|
101
|
+
# Build #brick_descrip if needed
|
102
|
+
if a.klass.instance_methods(false).exclude?(:brick_descrip)
|
103
|
+
descrip_col = (a.klass.columns.map(&:name) - a.klass._brick_get_fks -
|
104
|
+
(::Brick.config.metadata_columns || []) -
|
105
|
+
[a.klass.primary_key]).first&.to_sym
|
106
|
+
if descrip_col
|
107
|
+
a.klass.define_method :brick_descrip do
|
108
|
+
send(descrip_col)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
s.first[a.foreign_key] = [a.name, a.klass]
|
114
|
+
when :has_many, :has_one
|
115
|
+
s.last[a.name] = a
|
116
|
+
end
|
117
|
+
s
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
90
121
|
# Switches Brick auto-models on or off, for all threads
|
91
122
|
# @api public
|
92
123
|
def enable_models=(value)
|
@@ -156,8 +187,28 @@ module Brick
|
|
156
187
|
|
157
188
|
# Additional table associations to use (Think of these as virtual foreign keys perhaps)
|
158
189
|
# @api public
|
159
|
-
def additional_references=(
|
160
|
-
|
190
|
+
def additional_references=(ars)
|
191
|
+
if ars
|
192
|
+
ars = ars.call if ars.is_a?(Proc)
|
193
|
+
ars = ars.to_a unless ars.is_a?(Array)
|
194
|
+
ars = [ars] unless ars.empty? || ars.first.is_a?(Array)
|
195
|
+
Brick.config.additional_references = ars
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# Associations to treat has a has_one
|
200
|
+
# @api public
|
201
|
+
def has_ones=(hos)
|
202
|
+
if hos
|
203
|
+
hos = hos.call if hos.is_a?(Proc)
|
204
|
+
hos = hos.to_a unless hos.is_a?(Array)
|
205
|
+
hos = [hos] unless hos.empty? || hos.first.is_a?(Array)
|
206
|
+
# Translate to being nested hashes
|
207
|
+
Brick.config.has_ones = hos&.each_with_object(Hash.new { |h, k| h[k] = {} }) do |v, s|
|
208
|
+
s[v.first][v[1]] = v[2] if v[1]
|
209
|
+
s
|
210
|
+
end
|
211
|
+
end
|
161
212
|
end
|
162
213
|
|
163
214
|
|
@@ -197,10 +248,7 @@ module Brick
|
|
197
248
|
end
|
198
249
|
end
|
199
250
|
|
200
|
-
require 'brick/extensions'
|
201
251
|
require 'brick/version_number'
|
202
|
-
# require 'brick/serializers/json'
|
203
|
-
# require 'brick/serializers/yaml'
|
204
252
|
|
205
253
|
require 'active_record'
|
206
254
|
# Major compatibility fixes for ActiveRecord < 4.2
|
@@ -426,18 +474,18 @@ ActiveSupport.on_load(:active_record) do
|
|
426
474
|
end
|
427
475
|
end
|
428
476
|
|
429
|
-
include ::Brick::Extensions
|
430
|
-
|
431
|
-
unless ::Brick::Extensions::IS_AMOEBA
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
end
|
477
|
+
# include ::Brick::Extensions
|
478
|
+
|
479
|
+
# unless ::Brick::Extensions::IS_AMOEBA
|
480
|
+
# # Add amoeba-compatible support
|
481
|
+
# module ActiveRecord
|
482
|
+
# class Base
|
483
|
+
# def self.amoeba(*args)
|
484
|
+
# puts "Amoeba called from #{name} with #{args.inspect}"
|
485
|
+
# end
|
486
|
+
# end
|
487
|
+
# end
|
488
|
+
# end
|
441
489
|
end
|
442
490
|
|
443
491
|
# Do this earlier because stuff here gets mixed into JoinDependency::JoinAssociation and AssociationScope
|
@@ -526,3 +574,5 @@ if ActiveRecord.version < ::Gem::Version.new('5.2')
|
|
526
574
|
end # module ActiveRecord
|
527
575
|
# rubocop:enable Style/CommentedKeyword
|
528
576
|
end
|
577
|
+
|
578
|
+
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.
|
4
|
+
version: 1.0.6
|
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-
|
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
|