brick 1.0.75 → 1.0.77
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 +18 -0
- data/lib/brick/extensions.rb +299 -169
- data/lib/brick/frameworks/rails/engine.rb +89 -29
- data/lib/brick/version_number.rb +1 -1
- data/lib/brick.rb +39 -7
- data/lib/generators/brick/install_generator.rb +10 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7e113470d473585f716cfa5b835f503448a0d160b60de20f884bf3ce2cc14c08
|
4
|
+
data.tar.gz: 40064f980dd6893cb4e7e47861e00b184983d1a4bbc11b7cbd16fd6caddfab87
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4219dfee478a845a949b1ac9b03d0a135504723a892b571ca068af70c2b58ca52025f5e1f2fedb6a97a5b2dcd97280c5f54132b854e285e6daf7bc95d0c2d357
|
7
|
+
data.tar.gz: 67ae65b0de3801bbb2a1817d16b4023116ee7d41491b6d45927bd8197a8f800151f8ce79de34732a55f3dfcabbe301dc69b276899cfd544604d6a64e61220c99
|
data/lib/brick/config.rb
CHANGED
@@ -20,6 +20,15 @@ module Brick
|
|
20
20
|
@serializer = Brick::Serializers::YAML
|
21
21
|
end
|
22
22
|
|
23
|
+
# Any path prefixing to apply to all auto-generated Brick routes
|
24
|
+
def path_prefix
|
25
|
+
@mutex.synchronize { @path_prefix }
|
26
|
+
end
|
27
|
+
|
28
|
+
def path_prefix=(path)
|
29
|
+
@mutex.synchronize { @path_prefix = path }
|
30
|
+
end
|
31
|
+
|
23
32
|
# Indicates whether Brick models are on or off. Default: true.
|
24
33
|
def enable_models
|
25
34
|
@mutex.synchronize { !!@enable_models }
|
@@ -90,6 +99,15 @@ module Brick
|
|
90
99
|
@mutex.synchronize { @additional_references = references }
|
91
100
|
end
|
92
101
|
|
102
|
+
# Custom columns to add to a table, minimally defined with a name and DSL string
|
103
|
+
def custom_columns
|
104
|
+
@mutex.synchronize { @custom_columns }
|
105
|
+
end
|
106
|
+
|
107
|
+
def custom_columns=(cust_cols)
|
108
|
+
@mutex.synchronize { @custom_columns = cust_cols }
|
109
|
+
end
|
110
|
+
|
93
111
|
# Skip creating a has_many association for these
|
94
112
|
def exclude_hms
|
95
113
|
@mutex.synchronize { @exclude_hms }
|
data/lib/brick/extensions.rb
CHANGED
@@ -100,73 +100,86 @@ module ActiveRecord
|
|
100
100
|
dsl
|
101
101
|
end
|
102
102
|
|
103
|
-
def self.brick_parse_dsl(build_array = nil, prefix = [], translations = {},
|
104
|
-
|
105
|
-
|
103
|
+
def self.brick_parse_dsl(build_array = nil, prefix = [], translations = {}, is_polymorphic = false, dsl = nil, emit_dsl = false)
|
104
|
+
unless build_array.is_a?(::Brick::JoinArray)
|
105
|
+
build_array = ::Brick::JoinArray.new.tap { |ary| ary.replace([build_array]) } if build_array.is_a?(::Brick::JoinHash)
|
106
|
+
build_array = ::Brick::JoinArray.new unless build_array.nil? || build_array.is_a?(Array)
|
107
|
+
end
|
108
|
+
prefix = [prefix] unless prefix.is_a?(Array)
|
106
109
|
members = []
|
110
|
+
unless dsl || (dsl = ::Brick.config.model_descrips[name] || brick_get_dsl)
|
111
|
+
# With no DSL available, still put this prefix into the JoinArray so we can get primary key (ID) info from this table
|
112
|
+
x = prefix.each_with_object(build_array) { |v, s| s[v.to_sym] }
|
113
|
+
x[prefix.last] = nil unless prefix.empty? # Using []= will "hydrate" any missing part(s) in our whole series
|
114
|
+
return members
|
115
|
+
end
|
116
|
+
|
117
|
+
# Do the actual dirty work of recursing through nested DSL
|
107
118
|
bracket_name = nil
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
parts[0..-3].each { |v| s = s[v.to_sym] }
|
127
|
-
s[parts[-2]] = nil # unless parts[-2].empty? # Using []= will "hydrate" any missing part(s) in our whole series
|
128
|
-
end
|
129
|
-
translations[parts[0..-2].join('.')] = klass
|
119
|
+
dsl2 = +'' # To replace our own DSL definition in case it needs to be expanded
|
120
|
+
dsl3 = +'' # To return expanded DSL that is nested from another model
|
121
|
+
klass = nil
|
122
|
+
dsl.each_char do |ch|
|
123
|
+
if bracket_name
|
124
|
+
if ch == ']' # Time to process a bracketed thing?
|
125
|
+
parts = bracket_name.split('.')
|
126
|
+
first_parts = parts[0..-2].map do |part|
|
127
|
+
klass = (orig_class = klass).reflect_on_association(part_sym = part.to_sym)&.klass
|
128
|
+
puts "Couldn't reference #{orig_class.name}##{part} that's part of the DSL \"#{dsl}\"." if klass.nil?
|
129
|
+
part_sym
|
130
|
+
end
|
131
|
+
parts = prefix + first_parts + [parts[-1]]
|
132
|
+
if parts.length > 1
|
133
|
+
unless is_polymorphic
|
134
|
+
s = build_array
|
135
|
+
parts[0..-3].each { |v| s = s[v.to_sym] }
|
136
|
+
s[parts[-2]] = nil # unless parts[-2].empty? # Using []= will "hydrate" any missing part(s) in our whole series
|
130
137
|
end
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
dsl3 << dsl2a
|
138
|
-
else
|
139
|
-
dsl2 << "[#{bracket_name}]"
|
140
|
-
if emit_dsl
|
141
|
-
dsl3 << "[#{prefix[1..-1].map { |p| "#{p.to_s}." }.join if prefix.length > 1}#{bracket_name}]"
|
142
|
-
end
|
143
|
-
members << parts
|
138
|
+
translations[parts[0..-2].join('.')] = klass
|
139
|
+
end
|
140
|
+
if klass.column_names.exclude?(parts.last) &&
|
141
|
+
(klass = (orig_class = klass).reflect_on_association(possible_dsl = parts.pop.to_sym)&.klass)
|
142
|
+
if prefix.empty? # Custom columns start with an empty prefix
|
143
|
+
prefix << parts.shift until parts.empty?
|
144
144
|
end
|
145
|
-
|
145
|
+
# Expand this entry which refers to an association name
|
146
|
+
members2, dsl2a = klass.brick_parse_dsl(build_array, prefix + [possible_dsl], translations, is_polymorphic, nil, true)
|
147
|
+
members += members2
|
148
|
+
dsl2 << dsl2a
|
149
|
+
dsl3 << dsl2a
|
146
150
|
else
|
147
|
-
|
151
|
+
dsl2 << "[#{bracket_name}]"
|
152
|
+
if emit_dsl
|
153
|
+
dsl3 << "[#{prefix[1..-1].map { |p| "#{p.to_s}." }.join if prefix.length > 1}#{bracket_name}]"
|
154
|
+
end
|
155
|
+
members << parts
|
148
156
|
end
|
149
|
-
|
150
|
-
bracket_name = +''
|
151
|
-
klass = self
|
157
|
+
bracket_name = nil
|
152
158
|
else
|
153
|
-
|
154
|
-
dsl3 << ch
|
159
|
+
bracket_name << ch
|
155
160
|
end
|
161
|
+
elsif ch == '['
|
162
|
+
bracket_name = +''
|
163
|
+
klass = self
|
164
|
+
else
|
165
|
+
dsl2 << ch
|
166
|
+
dsl3 << ch
|
156
167
|
end
|
157
|
-
# Rewrite the DSL in case it's now different from having to expand it
|
158
|
-
# if ::Brick.config.model_descrips[name] != dsl2
|
159
|
-
# puts ::Brick.config.model_descrips[name]
|
160
|
-
# puts dsl2.inspect
|
161
|
-
# puts dsl3.inspect
|
162
|
-
# binding.pry
|
163
|
-
# end
|
164
|
-
::Brick.config.model_descrips[name] = dsl2 unless emit_dsl
|
165
|
-
else # With no DSL available, still put this prefix into the JoinArray so we can get primary key (ID) info from this table
|
166
|
-
x = prefix.each_with_object(build_array) { |v, s| s[v.to_sym] }
|
167
|
-
x[prefix.last] = nil unless prefix.empty? # Using []= will "hydrate" any missing part(s) in our whole series
|
168
168
|
end
|
169
|
-
|
169
|
+
# Rewrite the DSL in case it's now different from having to expand it
|
170
|
+
# if ::Brick.config.model_descrips[name] != dsl2
|
171
|
+
# puts ::Brick.config.model_descrips[name]
|
172
|
+
# puts dsl2.inspect
|
173
|
+
# puts dsl3.inspect
|
174
|
+
# binding.pry
|
175
|
+
# end
|
176
|
+
if emit_dsl
|
177
|
+
# Had been: [members, dsl2, dsl3]
|
178
|
+
[members, dsl3]
|
179
|
+
else
|
180
|
+
::Brick.config.model_descrips[name] = dsl2
|
181
|
+
members
|
182
|
+
end
|
170
183
|
end
|
171
184
|
|
172
185
|
# If available, parse simple DSL attached to a model in order to provide a friendlier name.
|
@@ -177,7 +190,8 @@ module ActiveRecord
|
|
177
190
|
end
|
178
191
|
|
179
192
|
def self.brick_descrip(obj, data = nil, pk_alias = nil)
|
180
|
-
|
193
|
+
dsl = obj if obj.is_a?(String)
|
194
|
+
if (dsl ||= ::Brick.config.model_descrips[(klass = self).name] || klass.brick_get_dsl)
|
181
195
|
idx = -1
|
182
196
|
caches = {}
|
183
197
|
output = +''
|
@@ -242,12 +256,13 @@ module ActiveRecord
|
|
242
256
|
table_name == assoc_name ? link : "#{assoc_name}-#{link}".html_safe
|
243
257
|
end
|
244
258
|
|
245
|
-
def self._brick_index
|
246
|
-
tbl_parts = table_name.split('.')
|
259
|
+
def self._brick_index(mode = nil)
|
260
|
+
tbl_parts = ((mode == :singular) ? table_name.singularize : table_name).split('.')
|
247
261
|
tbl_parts.shift if ::Brick.apartment_multitenant && tbl_parts.first == Apartment.default_schema
|
248
|
-
|
249
|
-
|
250
|
-
|
262
|
+
tbl_parts.unshift(::Brick.config.path_prefix) if ::Brick.config.path_prefix
|
263
|
+
index = tbl_parts.map(&:underscore).join('_')
|
264
|
+
# Rails applies an _index suffix to that route when the resource name is singular
|
265
|
+
index << '_index' if mode != :singular && index == index.singularize
|
251
266
|
index
|
252
267
|
end
|
253
268
|
|
@@ -272,18 +287,28 @@ module ActiveRecord
|
|
272
287
|
def _br_associatives
|
273
288
|
@_br_associatives ||= {}
|
274
289
|
end
|
290
|
+
# Custom columns
|
291
|
+
def _br_cust_cols
|
292
|
+
@_br_cust_cols ||= {}
|
293
|
+
end
|
275
294
|
end
|
276
295
|
|
277
|
-
# Search for BT, HM, and HMT DSL stuff
|
296
|
+
# Search for custom column, BT, HM, and HMT DSL stuff
|
278
297
|
def self._brick_calculate_bts_hms(translations, join_array)
|
298
|
+
# Add any custom columns
|
299
|
+
::Brick.config.custom_columns&.fetch(table_name, nil)&.each do |k, cc|
|
300
|
+
# false = not polymorphic, and true = yes -- please emit_dsl
|
301
|
+
pieces, my_dsl = brick_parse_dsl(join_array, [], translations, false, cc, true)
|
302
|
+
_br_cust_cols[k] = [pieces, my_dsl]
|
303
|
+
end
|
279
304
|
bts, hms, associatives = ::Brick.get_bts_and_hms(self)
|
280
305
|
bts.each do |_k, bt|
|
281
306
|
next if bt[2] # Polymorphic?
|
282
307
|
|
283
308
|
# join_array will receive this relation name when calling #brick_parse_dsl
|
284
309
|
_br_bt_descrip[bt.first] = if bt[1].is_a?(Array)
|
285
|
-
# Last
|
286
|
-
bt[1].each_with_object({}) { |bt_class, s| s[bt_class] = bt_class.brick_parse_dsl(join_array, bt.first, translations,
|
310
|
+
# Last params here: "true" is for yes, we are polymorphic
|
311
|
+
bt[1].each_with_object({}) { |bt_class, s| s[bt_class] = bt_class.brick_parse_dsl(join_array, bt.first, translations, true) }
|
287
312
|
else
|
288
313
|
{ bt.last => bt[1].brick_parse_dsl(join_array, bt.first, translations) }
|
289
314
|
end
|
@@ -314,7 +339,7 @@ module ActiveRecord
|
|
314
339
|
else # Expecting only Symbol
|
315
340
|
if _br_hm_counts.key?(ord_part)
|
316
341
|
ord_part = "\"b_r_#{ord_part}_ct\""
|
317
|
-
elsif !_br_bt_descrip.key?(ord_part) && !column_names.include?(ord_part.to_s)
|
342
|
+
elsif !_br_bt_descrip.key?(ord_part) && !_br_cust_cols.key?(ord_part) && !column_names.include?(ord_part.to_s)
|
318
343
|
# Disallow ordering by a bogus column
|
319
344
|
# %%% Note this bogus entry so that Javascript can remove any bogus _brick_order
|
320
345
|
# parameter from the querystring, pushing it into the browser history.
|
@@ -331,6 +356,11 @@ module ActiveRecord
|
|
331
356
|
[order_by, order_by_txt]
|
332
357
|
end
|
333
358
|
|
359
|
+
def self.brick_select(params = {}, selects = [], *args)
|
360
|
+
(relation = all).brick_select(params, selects, *args)
|
361
|
+
relation.select(selects)
|
362
|
+
end
|
363
|
+
|
334
364
|
private
|
335
365
|
|
336
366
|
def self._brick_get_fks
|
@@ -421,7 +451,13 @@ module ActiveRecord
|
|
421
451
|
end
|
422
452
|
end
|
423
453
|
|
424
|
-
def brick_select(params, selects =
|
454
|
+
def brick_select(params, selects = [], order_by = nil, translations = {}, join_array = ::Brick::JoinArray.new)
|
455
|
+
is_add_bts = is_add_hms = true
|
456
|
+
|
457
|
+
# Build out cust_cols, bt_descrip and hm_counts now so that they are available on the
|
458
|
+
# model early in case the user wants to do an ORDER BY based on any of that.
|
459
|
+
model._brick_calculate_bts_hms(translations, join_array) if is_add_bts || is_add_hms
|
460
|
+
|
425
461
|
is_postgres = ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
|
426
462
|
is_mysql = ActiveRecord::Base.connection.adapter_name == 'Mysql2'
|
427
463
|
is_mssql = ActiveRecord::Base.connection.adapter_name == 'SQLServer'
|
@@ -446,7 +482,7 @@ module ActiveRecord
|
|
446
482
|
end
|
447
483
|
|
448
484
|
# %%% Skip the metadata columns
|
449
|
-
if selects
|
485
|
+
if selects.empty? # Default to all columns
|
450
486
|
tbl_no_schema = table.name.split('.').last
|
451
487
|
# %%% Have once gotten this error with MSSQL referring to http://localhost:3000/warehouse/cold_room_temperatures__archive
|
452
488
|
# ActiveRecord::StatementInvalid (TinyTds::Error: DBPROCESS is dead or not enabled)
|
@@ -481,7 +517,36 @@ module ActiveRecord
|
|
481
517
|
id_for_tables = Hash.new { |h, k| h[k] = [] }
|
482
518
|
field_tbl_names = Hash.new { |h, k| h[k] = {} }
|
483
519
|
used_col_aliases = {} # Used to make sure there is not a name clash
|
484
|
-
|
520
|
+
|
521
|
+
# CUSTOM COLUMNS
|
522
|
+
# ==============
|
523
|
+
klass._br_cust_cols.each do |k, cc|
|
524
|
+
if respond_to?(k) # Name already taken?
|
525
|
+
# %%% Use ensure_unique here in this kind of fashion:
|
526
|
+
# cnstr_name = ensure_unique(+"(brick) #{for_tbl}_#{pri_tbl}", bts, hms)
|
527
|
+
# binding.pry
|
528
|
+
next
|
529
|
+
end
|
530
|
+
|
531
|
+
cc.first.each do |cc_part|
|
532
|
+
dest_klass = cc_part[0..-2].inject(klass) { |kl, cc_part_term| kl.reflect_on_association(cc_part_term).klass }
|
533
|
+
tbl_name = (field_tbl_names[k][cc_part.last] ||= shift_or_first(chains[dest_klass])).split('.').last
|
534
|
+
# Deal with the conflict if there are two parts in the custom column named the same,
|
535
|
+
# "category.name" and "product.name" for instance will end up with aliases of "name"
|
536
|
+
# and "product__name".
|
537
|
+
cc_part_idx = cc_part.length - 1
|
538
|
+
while cc_part_idx > 0 &&
|
539
|
+
(col_alias = "br_cc_#{k}__#{cc_part[cc_part_idx..-1].map(&:to_s).join('__')}") &&
|
540
|
+
used_col_aliases.key?(col_alias)
|
541
|
+
cc_part_idx -= 1
|
542
|
+
end
|
543
|
+
selects << "#{tbl_name}.#{cc_part.last} AS #{col_alias}"
|
544
|
+
cc_part << col_alias
|
545
|
+
used_col_aliases[col_alias] = nil
|
546
|
+
end
|
547
|
+
end
|
548
|
+
|
549
|
+
klass._br_bt_descrip.each do |v|
|
485
550
|
v.last.each do |k1, v1| # k1 is class, v1 is array of columns to snag
|
486
551
|
next if chains[k1].nil?
|
487
552
|
|
@@ -544,7 +609,13 @@ module ActiveRecord
|
|
544
609
|
klass._br_hm_counts.each do |k, hm|
|
545
610
|
associative = nil
|
546
611
|
count_column = if hm.options[:through]
|
547
|
-
|
612
|
+
if (fk_col = (associative = klass._br_associatives&.[](hm.name))&.foreign_key)
|
613
|
+
if hm.source_reflection.macro == :belongs_to # Traditional HMT using an associative table
|
614
|
+
hm.foreign_key
|
615
|
+
else # A HMT that goes HM -> HM, something like Categories -> Products -> LineItems
|
616
|
+
hm.source_reflection.active_record.primary_key
|
617
|
+
end
|
618
|
+
end
|
548
619
|
else
|
549
620
|
fk_col = hm.foreign_key
|
550
621
|
poly_type = hm.inverse_of.foreign_type if hm.options.key?(:as)
|
@@ -596,7 +667,8 @@ JOIN (SELECT #{selects.join(', ')}, COUNT(#{'DISTINCT ' if hm.options[:through]}
|
|
596
667
|
end
|
597
668
|
where!(wheres) unless wheres.empty?
|
598
669
|
# Must parse the order_by and see if there are any symbols which refer to BT associations
|
599
|
-
# as they must be expanded to find the corresponding b_r_model__column
|
670
|
+
# or custom columns as they must be expanded to find the corresponding b_r_model__column
|
671
|
+
# or br_cc_column naming for each.
|
600
672
|
if order_by.present?
|
601
673
|
final_order_by = *order_by.each_with_object([]) do |v, s|
|
602
674
|
if v.is_a?(Symbol)
|
@@ -605,6 +677,8 @@ JOIN (SELECT #{selects.join(', ')}, COUNT(#{'DISTINCT ' if hm.options[:through]}
|
|
605
677
|
bt_cols.values.each do |v1|
|
606
678
|
v1.each { |v2| s << "\"#{v2.last}\"" if v2.length > 1 }
|
607
679
|
end
|
680
|
+
elsif (cc_cols = klass._br_cust_cols[v])
|
681
|
+
cc_cols.first.each { |v1| s << "\"#{v1.last}\"" if v1.length > 1 }
|
608
682
|
else
|
609
683
|
s << v
|
610
684
|
end
|
@@ -710,50 +784,60 @@ end
|
|
710
784
|
Module.class_exec do
|
711
785
|
alias _brick_const_missing const_missing
|
712
786
|
def const_missing(*args)
|
713
|
-
|
787
|
+
requested = args.first.to_s
|
788
|
+
is_controller = requested.end_with?('Controller')
|
789
|
+
# self.name is nil when a model name is requested in an .erb file
|
790
|
+
if self.name && ::Brick.config.path_prefix
|
791
|
+
camelize_prefix = ::Brick.config.path_prefix.camelize
|
792
|
+
# Asking for the prefix module?
|
793
|
+
if self == Object && requested == camelize_prefix
|
794
|
+
Object.const_set(args.first, (built_module = Module.new))
|
795
|
+
puts "module #{camelize_prefix}; end\n"
|
796
|
+
return built_module
|
797
|
+
end
|
798
|
+
split_self_name.shift if (split_self_name = self.name.split('::')).first.blank?
|
799
|
+
if split_self_name.first == camelize_prefix
|
800
|
+
split_self_name.shift # Remove the identified path prefix from the split name
|
801
|
+
if is_controller
|
802
|
+
brick_root = split_self_name.empty? ? self : camelize_prefix.constantize
|
803
|
+
end
|
804
|
+
end
|
805
|
+
end
|
806
|
+
base_module = if self < ActiveRecord::Migration || !self.name
|
807
|
+
brick_root || Object
|
808
|
+
elsif (split_self_name || self.name.split('::')).length > 1
|
809
|
+
return self._brick_const_missing(*args)
|
810
|
+
else
|
811
|
+
self
|
812
|
+
end
|
813
|
+
desired_classname = (self == Object) ? requested : "#{name}::#{requested}"
|
714
814
|
if ((is_defined = self.const_defined?(args.first)) && (possible = self.const_get(args.first)) && possible.name == desired_classname) ||
|
715
815
|
# Try to require the respective Ruby file
|
716
816
|
((filename = ActiveSupport::Dependencies.search_for_file(desired_classname.underscore) ||
|
717
|
-
(self != Object && ActiveSupport::Dependencies.search_for_file((desired_classname =
|
817
|
+
(self != Object && ActiveSupport::Dependencies.search_for_file((desired_classname = requested).underscore))
|
718
818
|
) && (require_dependency(filename) || true) &&
|
719
819
|
((possible = self.const_get(args.first)) && possible.name == desired_classname)
|
720
820
|
) ||
|
721
821
|
# If any class has turned up so far (and we're not in the middle of eager loading)
|
722
822
|
# then return what we've found.
|
723
|
-
(is_defined && !::Brick.is_eager_loading)
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
|
729
|
-
|
730
|
-
# checks for it in ~/.rvm/gems/ruby-2.7.5/gems/activesupport-5.2.6.2/lib/active_support/dependencies.rb
|
731
|
-
# that is, checking #qualified_name_for with: from_mod, const_name
|
732
|
-
# If we want to support namespacing in the future, might have to utilise something like this:
|
733
|
-
# path_suffix = ActiveSupport::Dependencies.qualified_name_for(Object, args.first).underscore
|
734
|
-
# return self._brick_const_missing(*args) if ActiveSupport::Dependencies.search_for_file(path_suffix)
|
735
|
-
# If the file really exists, go and snag it:
|
736
|
-
if ActiveSupport::Dependencies.search_for_file(class_name.underscore)
|
737
|
-
return base_module._brick_const_missing(*args)
|
738
|
-
# elsif ActiveSupport::Dependencies.search_for_file(filepath) # Last-ditch effort to pick this thing up before we fill in the gaps on our own
|
739
|
-
# my_const = parent.const_missing(class_name) # ends up having: MyModule::MyClass
|
740
|
-
# return my_const
|
741
|
-
else
|
742
|
-
filepath = base_module.name&.split('::')&.[](0..-2) unless base_module == Object
|
743
|
-
filepath = ((filepath || []) + [class_name]).join('/').underscore + '.rb'
|
744
|
-
if ActiveSupport::Dependencies.search_for_file(filepath) # Last-ditch effort to pick this thing up before we fill in the gaps on our own
|
745
|
-
return base_module._brick_const_missing(*args)
|
823
|
+
(is_defined && !::Brick.is_eager_loading) # Used to also have: && possible != self
|
824
|
+
if (!brick_root && (filename || possible.instance_of?(Class))) ||
|
825
|
+
(possible.instance_of?(Module) &&
|
826
|
+
((possible.respond_to?(:module_parent) ? possible.module_parent : possible.parent) == self)
|
827
|
+
) ||
|
828
|
+
(possible.instance_of?(Class) && possible == self) # Are we simply searching for ourselves?
|
829
|
+
return possible
|
746
830
|
end
|
747
831
|
end
|
748
|
-
|
832
|
+
class_name = ::Brick.namify(requested)
|
749
833
|
relations = ::Brick.relations
|
750
|
-
|
751
|
-
|
834
|
+
result = if ::Brick.enable_controllers? &&
|
835
|
+
is_controller && (plural_class_name = class_name[0..-11]).length.positive?
|
752
836
|
# Otherwise now it's up to us to fill in the gaps
|
837
|
+
full_class_name = +''
|
838
|
+
full_class_name << "::#{(split_self_name&.first && split_self_name.join('::')) || self.name}" unless self == Object
|
753
839
|
# (Go over to underscores for a moment so that if we have something come in like VABCsController then the model name ends up as
|
754
840
|
# Vabc instead of VABC)
|
755
|
-
full_class_name = +''
|
756
|
-
full_class_name << "::#{self.name}" unless self == Object
|
757
841
|
singular_class_name = ::Brick.namify(plural_class_name, :underscore).singularize.camelize
|
758
842
|
full_class_name << "::#{singular_class_name}"
|
759
843
|
if plural_class_name == 'BrickSwagger' ||
|
@@ -856,7 +940,9 @@ class Object
|
|
856
940
|
schema_name
|
857
941
|
else
|
858
942
|
matching = "#{schema_name}.#{matching}"
|
859
|
-
|
943
|
+
# %%% Coming up with integers when tables are in schemas
|
944
|
+
# ::Brick.db_schemas[schema_name] ||= self.const_get(schema_name.camelize.to_sym)
|
945
|
+
self.const_get(schema_name.camelize)
|
860
946
|
end
|
861
947
|
"#{schema_module&.name}::#{inheritable_name || model_name}"
|
862
948
|
end
|
@@ -1124,8 +1210,7 @@ class Object
|
|
1124
1210
|
is_postgres = ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
|
1125
1211
|
is_mysql = ActiveRecord::Base.connection.adapter_name == 'Mysql2'
|
1126
1212
|
|
1127
|
-
|
1128
|
-
code = +"class #{namespace_name}#{class_name} < ApplicationController\n"
|
1213
|
+
code = +"class #{class_name} < ApplicationController\n"
|
1129
1214
|
built_controller = Class.new(ActionController::Base) do |new_controller_class|
|
1130
1215
|
(namespace || Object).const_set(class_name.to_sym, new_controller_class)
|
1131
1216
|
|
@@ -1204,22 +1289,6 @@ class Object
|
|
1204
1289
|
return
|
1205
1290
|
end
|
1206
1291
|
|
1207
|
-
# Normal (non-swagger) request
|
1208
|
-
|
1209
|
-
# We do all of this now so that bt_descrip and hm_counts are available on the model early in case the user
|
1210
|
-
# wants to do an ORDER BY based on any of that
|
1211
|
-
translations = {}
|
1212
|
-
join_array = ::Brick::JoinArray.new
|
1213
|
-
is_add_bts = is_add_hms = true
|
1214
|
-
# This builds out bt_descrip and hm_counts on the model
|
1215
|
-
model._brick_calculate_bts_hms(translations, join_array) if is_add_bts || is_add_hms
|
1216
|
-
|
1217
|
-
# %%% Allow params to define which columns to use for order_by
|
1218
|
-
# Overriding the default by providing a querystring param?
|
1219
|
-
ordering = params['_brick_order']&.split(',')&.map(&:to_sym) || Object.send(:default_ordering, table_name, pk)
|
1220
|
-
order_by, _ = model._brick_calculate_ordering(ordering, true) # Don't do the txt part
|
1221
|
-
|
1222
|
-
::Brick.set_db_schema(params)
|
1223
1292
|
if request.format == :csv # Asking for a template?
|
1224
1293
|
require 'csv'
|
1225
1294
|
exported_csv = CSV.generate(force_quotes: false) do |csv_out|
|
@@ -1233,7 +1302,16 @@ class Object
|
|
1233
1302
|
return
|
1234
1303
|
end
|
1235
1304
|
|
1236
|
-
|
1305
|
+
# Normal (not swagger or CSV) request
|
1306
|
+
|
1307
|
+
# %%% Allow params to define which columns to use for order_by
|
1308
|
+
# Overriding the default by providing a querystring param?
|
1309
|
+
ordering = params['_brick_order']&.split(',')&.map(&:to_sym) || Object.send(:default_ordering, table_name, pk)
|
1310
|
+
order_by, _ = model._brick_calculate_ordering(ordering, true) # Don't do the txt part
|
1311
|
+
|
1312
|
+
@_brick_params = (ar_relation = model.all).brick_select(params, (selects = []), nil,
|
1313
|
+
translations = {},
|
1314
|
+
join_array = ::Brick::JoinArray.new)
|
1237
1315
|
# %%% Add custom HM count columns
|
1238
1316
|
# %%% What happens when the PK is composite?
|
1239
1317
|
counts = model._br_hm_counts.each_with_object([]) do |v, s|
|
@@ -1275,13 +1353,6 @@ class Object
|
|
1275
1353
|
code << " end\n"
|
1276
1354
|
self.define_method :show do
|
1277
1355
|
::Brick.set_db_schema(params)
|
1278
|
-
id = if model.columns_hash[pk.first]&.type == :string
|
1279
|
-
is_pk_string = true
|
1280
|
-
params[:id]
|
1281
|
-
else
|
1282
|
-
params[:id]&.split(/[\/,_]/)
|
1283
|
-
end
|
1284
|
-
id = id.first if id.is_a?(Array) && id.length == 1
|
1285
1356
|
instance_variable_set("@#{singular_table_name}".to_sym, find_obj)
|
1286
1357
|
end
|
1287
1358
|
end
|
@@ -1376,7 +1447,14 @@ class Object
|
|
1376
1447
|
@#{singular_table_name} = #{model.name}.find(id.is_a?(Array) && id.length == 1 ? id.first : id)
|
1377
1448
|
end\n"
|
1378
1449
|
self.define_method :find_obj do
|
1379
|
-
id =
|
1450
|
+
id = if model.columns_hash[pk.first]&.type == :string
|
1451
|
+
is_pk_string = true
|
1452
|
+
params[:id].gsub('^^sl^^', '/')
|
1453
|
+
else
|
1454
|
+
params[:id]&.split(/[\/,_]/).map do |val_part|
|
1455
|
+
val_part.gsub('^^sl^^', '/')
|
1456
|
+
end
|
1457
|
+
end
|
1380
1458
|
model.find(id.is_a?(Array) && id.length == 1 ? id.first : id)
|
1381
1459
|
end
|
1382
1460
|
end
|
@@ -1399,33 +1477,55 @@ class Object
|
|
1399
1477
|
# Get column names for params from relations[model.table_name][:cols].keys
|
1400
1478
|
end
|
1401
1479
|
end # unless is_swagger
|
1402
|
-
code << "end # #{
|
1480
|
+
code << "end # #{class_name}\n"
|
1403
1481
|
end # class definition
|
1404
1482
|
[built_controller, code]
|
1405
1483
|
end
|
1406
1484
|
|
1407
1485
|
def _brick_get_hm_assoc_name(relation, hm_assoc, source = nil)
|
1408
|
-
if (relation[:hm_counts][hm_assoc[:inverse_table]]&.> 1) &&
|
1409
|
-
|
1410
|
-
|
1411
|
-
|
1412
|
-
|
1413
|
-
|
1414
|
-
|
1415
|
-
|
1416
|
-
|
1417
|
-
|
1418
|
-
|
1419
|
-
|
1420
|
-
|
1421
|
-
|
1422
|
-
|
1423
|
-
|
1424
|
-
|
1486
|
+
assoc_name, needs_class = if (relation[:hm_counts][hm_assoc[:inverse_table]]&.> 1) &&
|
1487
|
+
hm_assoc[:alternate_name] != (source || name.underscore)
|
1488
|
+
plural = "#{hm_assoc[:assoc_name]}_#{ActiveSupport::Inflector.pluralize(hm_assoc[:alternate_name])}"
|
1489
|
+
new_alt_name = (hm_assoc[:alternate_name] == name.underscore) ? "#{hm_assoc[:assoc_name].singularize}_#{plural}" : plural
|
1490
|
+
# uniq = 1
|
1491
|
+
# while same_name = relation[:fks].find { |x| x.last[:assoc_name] == hm_assoc[:assoc_name] && x.last != hm_assoc }
|
1492
|
+
# hm_assoc[:assoc_name] = "#{hm_assoc_name}_#{uniq += 1}"
|
1493
|
+
# end
|
1494
|
+
# puts new_alt_name
|
1495
|
+
# hm_assoc[:assoc_name] = new_alt_name
|
1496
|
+
[new_alt_name, true]
|
1497
|
+
else
|
1498
|
+
assoc_name = ::Brick.namify(hm_assoc[:inverse_table]).pluralize
|
1499
|
+
if (needs_class = assoc_name.include?('.')) # If there is a schema name present, use a downcased version for the :has_many
|
1500
|
+
assoc_parts = assoc_name.split('.')
|
1501
|
+
assoc_parts[0].downcase! if assoc_parts[0] =~ /^[A-Z0-9_]+$/
|
1502
|
+
assoc_name = assoc_parts.join('.')
|
1503
|
+
end
|
1504
|
+
# hm_assoc[:assoc_name] = assoc_name
|
1505
|
+
[assoc_name, needs_class]
|
1506
|
+
end
|
1507
|
+
# Already have the HM class around?
|
1508
|
+
begin
|
1509
|
+
if (hm_class = Object._brick_const_missing(hm_class_name = relation[:class_name].to_sym))
|
1510
|
+
existing_hm_assocs = hm_class.reflect_on_all_associations.select do |assoc|
|
1511
|
+
assoc.macro != :belongs_to && assoc.klass == self && assoc.foreign_key == hm_assoc[:fk]
|
1512
|
+
end
|
1513
|
+
# Missing a has_many in an existing class?
|
1514
|
+
if existing_hm_assocs.empty?
|
1515
|
+
options = { inverse_of: hm_assoc[:inverse][:assoc_name].to_sym }
|
1516
|
+
# Add class_name and foreign_key where necessary
|
1517
|
+
unless hm_assoc[:alternate_name] == (source || name.underscore)
|
1518
|
+
options[:class_name] = self.name
|
1519
|
+
options[:foreign_key] = hm_assoc[:fk].to_sym
|
1520
|
+
end
|
1521
|
+
hm_class.send(:has_many, assoc_name.to_sym, options)
|
1522
|
+
puts "# ** Adding a missing has_many to #{hm_class.name}:\nclass #{hm_class.name} < #{hm_class.superclass.name}"
|
1523
|
+
puts " has_many :#{assoc_name}, #{options.inspect}\nend\n"
|
1524
|
+
end
|
1425
1525
|
end
|
1426
|
-
|
1427
|
-
[assoc_name, needs_class]
|
1526
|
+
rescue NameError
|
1428
1527
|
end
|
1528
|
+
[assoc_name, needs_class]
|
1429
1529
|
end
|
1430
1530
|
end
|
1431
1531
|
end
|
@@ -1438,6 +1538,7 @@ module ActiveRecord::ConnectionHandling
|
|
1438
1538
|
alias _brick_establish_connection establish_connection
|
1439
1539
|
def establish_connection(*args)
|
1440
1540
|
conn = _brick_establish_connection(*args)
|
1541
|
+
begin
|
1441
1542
|
# Overwrite SQLite's #begin_db_transaction so it opens in IMMEDIATE mode instead of
|
1442
1543
|
# the default DEFERRED mode.
|
1443
1544
|
# https://discuss.rubyonrails.org/t/failed-write-transaction-upgrades-in-sqlite3/81480/2
|
@@ -1459,9 +1560,10 @@ module ActiveRecord::ConnectionHandling
|
|
1459
1560
|
end
|
1460
1561
|
end
|
1461
1562
|
end
|
1462
|
-
|
1563
|
+
# ::Brick.is_db_present = true
|
1463
1564
|
_brick_reflect_tables
|
1464
1565
|
rescue ActiveRecord::NoDatabaseError
|
1566
|
+
# ::Brick.is_db_present = false
|
1465
1567
|
end
|
1466
1568
|
conn
|
1467
1569
|
end
|
@@ -1469,6 +1571,8 @@ module ActiveRecord::ConnectionHandling
|
|
1469
1571
|
# This is done separately so that during testing it can be called right after a migration
|
1470
1572
|
# in order to make sure everything is good.
|
1471
1573
|
def _brick_reflect_tables
|
1574
|
+
# return if ActiveRecord::Base.connection.current_database == 'postgres'
|
1575
|
+
|
1472
1576
|
initializer_loaded = false
|
1473
1577
|
if (relations = ::Brick.relations).empty?
|
1474
1578
|
# If there's schema things configured then we only expect our initializer to be named exactly this
|
@@ -1477,8 +1581,8 @@ module ActiveRecord::ConnectionHandling
|
|
1477
1581
|
end
|
1478
1582
|
# Load the initializer for the Apartment gem a little early so that if .excluded_models and
|
1479
1583
|
# .default_schema are specified then we can work with non-tenanted models more appropriately
|
1480
|
-
apartment = Object.const_defined?('Apartment')
|
1481
|
-
|
1584
|
+
if (apartment = Object.const_defined?('Apartment')) &&
|
1585
|
+
File.exist?(apartment_initializer = ::Rails.root.join('config/initializers/apartment.rb'))
|
1482
1586
|
load apartment_initializer
|
1483
1587
|
apartment_excluded = Apartment.excluded_models
|
1484
1588
|
end
|
@@ -1490,19 +1594,21 @@ module ActiveRecord::ConnectionHandling
|
|
1490
1594
|
case ActiveRecord::Base.connection.adapter_name
|
1491
1595
|
when 'PostgreSQL', 'SQLServer'
|
1492
1596
|
is_postgres = !is_mssql
|
1493
|
-
db_schemas =
|
1597
|
+
db_schemas = if is_postgres
|
1598
|
+
ActiveRecord::Base.execute_sql('SELECT nspname AS table_schema, MAX(oid) AS dt FROM pg_namespace GROUP BY 1 ORDER BY 1;')
|
1599
|
+
else
|
1600
|
+
ActiveRecord::Base.execute_sql('SELECT DISTINCT table_schema, NULL AS dt FROM INFORMATION_SCHEMA.tables;')
|
1601
|
+
end
|
1494
1602
|
::Brick.db_schemas = db_schemas.each_with_object({}) do |row, s|
|
1495
1603
|
row = case row
|
1496
|
-
when String
|
1497
|
-
row
|
1498
1604
|
when Array
|
1499
|
-
row
|
1605
|
+
row
|
1500
1606
|
else
|
1501
|
-
row['table_schema']
|
1607
|
+
[row['table_schema'], row['dt']]
|
1502
1608
|
end
|
1503
1609
|
# Remove any system schemas
|
1504
|
-
s[row] =
|
1505
|
-
|
1610
|
+
s[row.first] = row.last unless ['information_schema', 'pg_catalog', 'pg_toast',
|
1611
|
+
'INFORMATION_SCHEMA', 'sys'].include?(row.first)
|
1506
1612
|
end
|
1507
1613
|
if (is_multitenant = (multitenancy = ::Brick.config.schema_behavior[:multitenant]) &&
|
1508
1614
|
(sta = multitenancy[:schema_to_analyse]) != 'public') &&
|
@@ -1536,6 +1642,10 @@ module ActiveRecord::ConnectionHandling
|
|
1536
1642
|
if ::Brick.db_schemas.key?(possible_schema)
|
1537
1643
|
::Brick.default_schema = schema = possible_schema
|
1538
1644
|
ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema)
|
1645
|
+
elsif Rails.env == 'test' # When testing, just find the most recently-created schema
|
1646
|
+
::Brick.default_schema = schema = ::Brick.db_schemas.to_a.sort { |a, b| b.last <=> a.last }.first.first
|
1647
|
+
puts "While running tests, had noticed that in the brick.rb initializer the line \"::Brick.schema_behavior = ...\" refers to a schema called \"#{possible_schema}\" which does not exist. Reverting to instead use the most recently-created schema, #{schema}."
|
1648
|
+
ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema)
|
1539
1649
|
else
|
1540
1650
|
puts "*** In the brick.rb initializer the line \"::Brick.schema_behavior = ...\" refers to a schema called \"#{possible_schema}\". This schema does not exist. ***"
|
1541
1651
|
end
|
@@ -1548,7 +1658,7 @@ module ActiveRecord::ConnectionHandling
|
|
1548
1658
|
measures = []
|
1549
1659
|
::Brick.is_oracle = true if ActiveRecord::Base.connection.adapter_name == 'OracleEnhanced'
|
1550
1660
|
case ActiveRecord::Base.connection.adapter_name
|
1551
|
-
when 'PostgreSQL', 'SQLite' # These bring back a hash for each row because the query uses column aliases
|
1661
|
+
when 'PostgreSQL', 'SQLite', 'SQLServer' # These bring back a hash for each row because the query uses column aliases
|
1552
1662
|
# schema ||= 'public' if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
|
1553
1663
|
ActiveRecord::Base.retrieve_schema_and_tables(sql, is_postgres, is_mssql, schema).each do |r|
|
1554
1664
|
# If Apartment gem lists the table as being associated with a non-tenanted model then use whatever it thinks
|
@@ -1757,7 +1867,7 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
|
|
1757
1867
|
LEFT OUTER JOIN INFORMATION_SCHEMA.columns AS c ON t.table_schema = c.table_schema
|
1758
1868
|
AND t.table_name = c.table_name
|
1759
1869
|
LEFT OUTER JOIN
|
1760
|
-
(SELECT kcu1.constraint_schema, kcu1.table_name, kcu1.ordinal_position,
|
1870
|
+
(SELECT kcu1.constraint_schema, kcu1.table_name, kcu1.column_name, kcu1.ordinal_position,
|
1761
1871
|
tc.constraint_type, kcu1.constraint_name
|
1762
1872
|
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu1
|
1763
1873
|
INNER JOIN INFORMATION_SCHEMA.table_constraints AS tc
|
@@ -1768,9 +1878,9 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
|
|
1768
1878
|
) AS kcu ON
|
1769
1879
|
-- kcu.CONSTRAINT_CATALOG = t.table_catalog AND
|
1770
1880
|
kcu.CONSTRAINT_SCHEMA = c.table_schema
|
1771
|
-
AND kcu.TABLE_NAME = c.table_name
|
1881
|
+
AND kcu.TABLE_NAME = c.table_name
|
1882
|
+
AND kcu.column_name = c.column_name#{"
|
1772
1883
|
-- AND kcu.position_in_unique_constraint IS NULL" unless is_mssql}
|
1773
|
-
AND kcu.ordinal_position = c.ordinal_position
|
1774
1884
|
WHERE t.table_schema #{is_postgres || is_mssql ?
|
1775
1885
|
"NOT IN ('information_schema', 'pg_catalog',
|
1776
1886
|
'INFORMATION_SCHEMA', 'sys')"
|
@@ -1779,7 +1889,7 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
|
|
1779
1889
|
AND t.table_schema = COALESCE(current_setting('SEARCH_PATH'), 'public')" if is_postgres && schema }
|
1780
1890
|
-- AND t.table_type IN ('VIEW') -- 'BASE TABLE', 'FOREIGN TABLE'
|
1781
1891
|
AND t.table_name NOT IN ('pg_stat_statements', ?, ?)
|
1782
|
-
ORDER BY 1, t.table_type DESC, 2,
|
1892
|
+
ORDER BY 1, t.table_type DESC, 2, kcu.ordinal_position"
|
1783
1893
|
ActiveRecord::Base.execute_sql(sql, *ar_tables)
|
1784
1894
|
end
|
1785
1895
|
|
@@ -1902,9 +2012,7 @@ module Brick
|
|
1902
2012
|
# For any appended references (those that come from config), arrive upon a definitely unique constraint name
|
1903
2013
|
pri_tbl = is_class ? fk[4][:class].underscore : pri_tbl
|
1904
2014
|
pri_tbl = "#{bt_assoc_name}_#{pri_tbl}" if pri_tbl&.singularize != bt_assoc_name
|
1905
|
-
|
1906
|
-
cnstr_added_num = 1
|
1907
|
-
cnstr_name = "#{cnstr_base}_#{cnstr_added_num += 1}" while bts&.key?(cnstr_name) || hms&.key?(cnstr_name)
|
2015
|
+
cnstr_name = ensure_unique(+"(brick) #{for_tbl}_#{pri_tbl}", bts, hms)
|
1908
2016
|
missing = []
|
1909
2017
|
missing << fk[1] unless relations.key?(fk[1])
|
1910
2018
|
missing << primary_table unless is_class || relations.key?(primary_table)
|
@@ -2026,7 +2134,29 @@ module Brick
|
|
2026
2134
|
end
|
2027
2135
|
end
|
2028
2136
|
end
|
2029
|
-
::Brick.relations.keys.map { |v| [(
|
2137
|
+
::Brick.relations.keys.map { |v| [(model = models[v])&.last, model&.last&.table_name, migrations&.fetch(v, nil), model&.first] }
|
2138
|
+
end
|
2139
|
+
|
2140
|
+
def ensure_unique(name, *sources)
|
2141
|
+
base = name
|
2142
|
+
if (added_num = name.slice!(/_(\d+)$/))
|
2143
|
+
added_num = added_num[1..-1].to_i
|
2144
|
+
else
|
2145
|
+
added_num = 1
|
2146
|
+
end
|
2147
|
+
while (
|
2148
|
+
name = "#{base}_#{added_num += 1}"
|
2149
|
+
sources.each_with_object(nil) do |v, s|
|
2150
|
+
s || case v
|
2151
|
+
when Hash
|
2152
|
+
v.key?(name)
|
2153
|
+
when Array
|
2154
|
+
v.include?(name)
|
2155
|
+
end
|
2156
|
+
end
|
2157
|
+
)
|
2158
|
+
end
|
2159
|
+
name
|
2030
2160
|
end
|
2031
2161
|
|
2032
2162
|
# Locate orphaned records
|
@@ -33,6 +33,9 @@ module Brick
|
|
33
33
|
# Additional references (virtual foreign keys)
|
34
34
|
::Brick.additional_references = app.config.brick.fetch(:additional_references, nil)
|
35
35
|
|
36
|
+
# Custom columns to add to a table, minimally defined with a name and DSL string
|
37
|
+
::Brick.custom_columns = app.config.brick.fetch(:custom_columns, nil)
|
38
|
+
|
36
39
|
# When table names have specific prefixes, automatically place them in their own module with a table_name_prefix.
|
37
40
|
::Brick.order = app.config.brick.fetch(:order, {})
|
38
41
|
|
@@ -111,7 +114,7 @@ module Brick
|
|
111
114
|
if @_brick_model
|
112
115
|
pk = @_brick_model._brick_primary_key(::Brick.relations.fetch(@_brick_model&.table_name, nil))
|
113
116
|
obj_name = model_name.split('::').last.underscore
|
114
|
-
path_obj_name =
|
117
|
+
path_obj_name = @_brick_model._brick_index(:singular)
|
115
118
|
table_name = obj_name.pluralize
|
116
119
|
template_link = nil
|
117
120
|
bts, hms = ::Brick.get_bts_and_hms(@_brick_model) # This gets BT and HM and also has_many :through (HMT)
|
@@ -122,9 +125,11 @@ module Brick
|
|
122
125
|
"H#{hm_assoc.macro == :has_one ? 'O' : 'M'}#{'T' if hm_assoc.options[:through]}",
|
123
126
|
(assoc_name = hm.first)]
|
124
127
|
hm_fk_name = if (through = hm_assoc.options[:through])
|
125
|
-
|
128
|
+
# %%% How to deal with weird self_ref type has_many -> has_one polymorphic stuff?
|
129
|
+
# (or perhaps we don't need to!)
|
130
|
+
next unless @_brick_model.instance_methods.include?(through) &&
|
131
|
+
(associative = @_brick_model._br_associatives.fetch(hm.first, nil))
|
126
132
|
|
127
|
-
associative = @_brick_model._br_associatives[hm.first]
|
128
133
|
tbl_nm = if hm_assoc.options[:source]
|
129
134
|
associative.klass.reflect_on_association(hm_assoc.options[:source]).inverse_of&.name
|
130
135
|
else
|
@@ -174,6 +179,7 @@ module Brick
|
|
174
179
|
# %%% If we are not auto-creating controllers (or routes) then omit by default, and if enabled anyway, such as in a development
|
175
180
|
# environment or whatever, then get either the controllers or routes list instead
|
176
181
|
apartment_default_schema = ::Brick.apartment_multitenant && Apartment.default_schema
|
182
|
+
prefix = "#{::Brick.config.path_prefix}/" if ::Brick.config.path_prefix
|
177
183
|
table_options = (::Brick.relations.keys - ::Brick.config.exclude_tables).each_with_object({}) do |tbl, s|
|
178
184
|
binding.pry if tbl.is_a?(Symbol)
|
179
185
|
if (tbl_parts = tbl.split('.')).first == apartment_default_schema
|
@@ -181,7 +187,7 @@ module Brick
|
|
181
187
|
end
|
182
188
|
s[tbl] = nil
|
183
189
|
end.keys.sort.each_with_object(+'') do |v, s|
|
184
|
-
s << "<option value=\"#{v.underscore.gsub('.', '/')}\">#{v}</option>"
|
190
|
+
s << "<option value=\"#{prefix}#{v.underscore.gsub('.', '/')}\">#{v}</option>"
|
185
191
|
end.html_safe
|
186
192
|
table_options << '<option value="brick_status">(Status)</option>'.html_safe if ::Brick.config.add_status
|
187
193
|
table_options << '<option value="brick_orphans">(Orphans)</option>'.html_safe if is_orphans
|
@@ -249,8 +255,8 @@ tr th {
|
|
249
255
|
right: 0;
|
250
256
|
cursor: pointer;
|
251
257
|
}
|
252
|
-
#headerTop tr th:hover {
|
253
|
-
background-color: #
|
258
|
+
#headerTop tr th:hover, #headerTop tr th.highlight {
|
259
|
+
background-color: #28B898;
|
254
260
|
}
|
255
261
|
#exclusions {
|
256
262
|
font-size: 0.7em;
|
@@ -273,6 +279,10 @@ tr th, tr td {
|
|
273
279
|
padding: 0.2em 0.5em;
|
274
280
|
}
|
275
281
|
|
282
|
+
tr td.highlight {
|
283
|
+
background-color: #B0B0FF;
|
284
|
+
}
|
285
|
+
|
276
286
|
.show-field {
|
277
287
|
background-color: #004998;
|
278
288
|
}
|
@@ -398,6 +408,11 @@ def display_value(col_type, val)
|
|
398
408
|
end
|
399
409
|
end
|
400
410
|
end
|
411
|
+
# Accommodate composite primary keys that include strings with forward-slash characters
|
412
|
+
def slashify(val)
|
413
|
+
val = [val] unless val.is_a?(Array)
|
414
|
+
val.map { |val_part| val_part.is_a?(String) ? val_part.gsub('/', '^^sl^^') : val_part }
|
415
|
+
end
|
401
416
|
callbacks = {} %>"
|
402
417
|
|
403
418
|
if ['index', 'show', 'new', 'update'].include?(args.first)
|
@@ -454,7 +469,7 @@ window.addEventListener(\"pageshow\", function() {
|
|
454
469
|
});
|
455
470
|
|
456
471
|
if (tblSelect) { // Always present
|
457
|
-
var i = schemaSelect ? 1 : 0,
|
472
|
+
var i = #{::Brick.config.path_prefix ? '0' : 'schemaSelect ? 1 : 0'},
|
458
473
|
changeoutList = changeout(location.href);
|
459
474
|
for (; i < changeoutList.length; ++i) {
|
460
475
|
tblSelect.value = changeoutList[i];
|
@@ -500,6 +515,38 @@ function changeout(href, param, value, trimAfter) {
|
|
500
515
|
var grid = document.getElementById(\"#{table_name}\");
|
501
516
|
#{table_name}HtColumns = grid && [grid.getElementsByTagName(\"TR\")[0]];
|
502
517
|
var headerTop = document.getElementById(\"headerTop\");
|
518
|
+
var headerCols;
|
519
|
+
if (grid) {
|
520
|
+
// COLUMN HEADER AND TABLE CELL HIGHLIGHTING
|
521
|
+
var gridHighHeader = null,
|
522
|
+
gridHighCell = null;
|
523
|
+
grid.addEventListener(\"mouseenter\", gridMove);
|
524
|
+
grid.addEventListener(\"mousemove\", gridMove);
|
525
|
+
grid.addEventListener(\"mouseleave\", function (evt) {
|
526
|
+
if (gridHighCell) gridHighCell.classList.remove(\"highlight\");
|
527
|
+
gridHighCell = null;
|
528
|
+
if (gridHighHeader) gridHighHeader.classList.remove(\"highlight\");
|
529
|
+
gridHighHeader = null;
|
530
|
+
});
|
531
|
+
function gridMove(evt) {
|
532
|
+
var lastHighCell = gridHighCell;
|
533
|
+
gridHighCell = document.elementFromPoint(evt.x, evt.y);
|
534
|
+
while (gridHighCell && gridHighCell.tagName !== \"TD\" && gridHighCell.tagName !== \"TH\")
|
535
|
+
gridHighCell = gridHighCell.parentElement;
|
536
|
+
if (gridHighCell) {
|
537
|
+
if (lastHighCell !== gridHighCell) {
|
538
|
+
gridHighCell.classList.add(\"highlight\");
|
539
|
+
if (lastHighCell) lastHighCell.classList.remove(\"highlight\");
|
540
|
+
}
|
541
|
+
var lastHighHeader = gridHighHeader;
|
542
|
+
gridHighHeader = headerCols[gridHighCell.cellIndex];
|
543
|
+
if (lastHighHeader !== gridHighHeader) {
|
544
|
+
if (gridHighHeader) gridHighHeader.classList.add(\"highlight\");
|
545
|
+
if (lastHighHeader) lastHighHeader.classList.remove(\"highlight\");
|
546
|
+
}
|
547
|
+
}
|
548
|
+
}
|
549
|
+
}
|
503
550
|
function setHeaderSizes() {
|
504
551
|
// console.log(\"start\");
|
505
552
|
// See if the headerTop is already populated
|
@@ -531,6 +578,7 @@ function setHeaderSizes() {
|
|
531
578
|
}
|
532
579
|
}
|
533
580
|
}
|
581
|
+
headerCols = tr.childNodes;
|
534
582
|
if (isEmpty) headerTop.appendChild(tr);
|
535
583
|
}
|
536
584
|
grid.style.marginTop = \"-\" + getComputedStyle(headerTop).height;
|
@@ -779,7 +827,8 @@ erDiagram
|
|
779
827
|
cols[col_name] = col
|
780
828
|
end
|
781
829
|
unless @_brick_sequence # If no sequence is defined, start with all inclusions
|
782
|
-
|
830
|
+
cust_cols = #{model_name}._br_cust_cols
|
831
|
+
@_brick_sequence = col_keys + cust_cols.keys + #{(hms_keys).inspect}.reject { |assoc_name| @_brick_incl&.exclude?(assoc_name) }
|
783
832
|
end
|
784
833
|
@_brick_sequence.reject! { |nm| @_brick_excl.include?(nm) } if @_brick_excl # Reject exclusions
|
785
834
|
@_brick_sequence.each_with_object(+'') do |col_name, s|
|
@@ -796,6 +845,8 @@ erDiagram
|
|
796
845
|
elsif col # HM column
|
797
846
|
s << \"<th#\{' x-order=\"' + col_name + '\"' if true}>#\{col[2]} \"
|
798
847
|
s << (col.first ? \"#\{col[3]}\" : \"#\{link_to(col[3], send(\"#\{col[1]._brick_index}_path\"))}\")
|
848
|
+
elsif (cc = cust_cols.key?(col_name)) # Custom column
|
849
|
+
s << \"<th x-order=\\\"#\{col_name}\\\">#\{col_name}\"
|
799
850
|
else # Bad column name!
|
800
851
|
s << \"<th title=\\\"<< Unknown column >>\\\">#\{col_name}\"
|
801
852
|
end
|
@@ -810,24 +861,25 @@ erDiagram
|
|
810
861
|
@#{table_name}.each do |#{obj_name}|
|
811
862
|
hms_cols = {#{hms_columns.join(', ')}} %>
|
812
863
|
<tr>#{"
|
813
|
-
<td><%= link_to '⇛', #{path_obj_name}_path(#{obj_pk}), { class: 'big-arrow' } %></td>" if obj_pk}
|
864
|
+
<td><%= link_to '⇛', #{path_obj_name}_path(slashify(#{obj_pk})), { class: 'big-arrow' } %></td>" if obj_pk}
|
814
865
|
<% @_brick_sequence.each do |col_name|
|
815
866
|
val = #{obj_name}.attributes[col_name] %>
|
816
|
-
<td<%= ' class=\"dimmed\"'.html_safe unless cols.key?(col_name)%>><%
|
867
|
+
<td<%= ' class=\"dimmed\"'.html_safe unless cols.key?(col_name) || (cust_col = cust_cols[col_name])%>><%
|
817
868
|
if (bt = bts[col_name])
|
818
869
|
if bt[2] # Polymorphic?
|
819
|
-
bt_class = #{obj_name}.send(\"#\{bt.first
|
820
|
-
|
821
|
-
poly_id = #{obj_name}.send(\"#\{bt.first
|
822
|
-
%><%= link_to(\"#\{bt_class
|
870
|
+
bt_class = #{obj_name}.send(\"#\{bt.first}_type\")
|
871
|
+
base_class_underscored = (::Brick.existing_stis[bt_class] || bt_class).constantize.base_class._brick_index(:singular)
|
872
|
+
poly_id = #{obj_name}.send(\"#\{bt.first}_id\")
|
873
|
+
%><%= link_to(\"#\{bt_class} ##\{poly_id}\", send(\"#\{base_class_underscored}_path\".to_sym, poly_id)) if poly_id %><%
|
823
874
|
else
|
875
|
+
# binding.pry if @_brick_bt_descrip[bt.first][bt[1].first.first].nil?
|
824
876
|
bt_txt = (bt_class = bt[1].first.first).brick_descrip(
|
825
877
|
# 0..62 because Postgres column names are limited to 63 characters
|
826
878
|
#{obj_name}, (descrips = @_brick_bt_descrip[bt.first][bt_class])[0..-2].map { |id| #{obj_name}.send(id.last[0..62]) }, (bt_id_col = descrips.last)
|
827
879
|
)
|
828
880
|
bt_txt ||= \"<span class=\\\"orphan\\\"><< Orphaned ID: #\{val} >></span>\".html_safe if val
|
829
881
|
bt_id = bt_id_col.map { |id_col| #{obj_name}.send(id_col.to_sym) } %>
|
830
|
-
<%= bt_id&.first ? link_to(bt_txt, send(\"#\{bt_class.base_class.
|
882
|
+
<%= bt_id&.first ? link_to(bt_txt, send(\"#\{bt_class.base_class._brick_index(:singular)}_path\".to_sym, bt_id)) : bt_txt %>
|
831
883
|
<% end
|
832
884
|
elsif (hms_col = hms_cols[col_name])
|
833
885
|
if hms_col.length == 1 %>
|
@@ -838,15 +890,18 @@ erDiagram
|
|
838
890
|
descrips = @_brick_bt_descrip[col_name.to_sym][klass]
|
839
891
|
ho_txt = klass.brick_descrip(#{obj_name}, descrips[0..-2].map { |id| #{obj_name}.send(id.last[0..62]) }, (ho_id_col = descrips.last))
|
840
892
|
ho_id = ho_id_col.map { |id_col| #{obj_name}.send(id_col.to_sym) }
|
841
|
-
ho_id&.first ? link_to(ho_txt, send(\"#\{klass.base_class.
|
893
|
+
ho_id&.first ? link_to(ho_txt, send(\"#\{klass.base_class._brick_index(:singular)}_path\".to_sym, ho_id)) : ho_txt
|
842
894
|
else
|
843
|
-
\"#\{hms_col[1] || 'View'
|
895
|
+
\"#\{hms_col[1] || 'View'} #\{hms_col.first}\"
|
844
896
|
end %>
|
845
897
|
<%= link_to txt, send(\"#\{klass._brick_index}_path\".to_sym, hms_col[2]) unless hms_col[1]&.zero? %>
|
846
898
|
<% end
|
847
899
|
elsif (col = cols[col_name])
|
848
900
|
col_type = col&.sql_type == 'geography' ? col.sql_type : col&.type
|
849
901
|
%><%= display_value(col_type || col&.sql_type, val) %><%
|
902
|
+
elsif cust_col
|
903
|
+
data = cust_col.first.map { |cc_part| #{obj_name}.send(cc_part.last) }
|
904
|
+
%><%= #{model_name}.brick_descrip(cust_col.last, data) %><%
|
850
905
|
else # Bad column name!
|
851
906
|
%>?<%
|
852
907
|
end
|
@@ -885,7 +940,7 @@ erDiagram
|
|
885
940
|
@resources.each do |r|
|
886
941
|
%>
|
887
942
|
<tr>
|
888
|
-
<td><%= link_to(r[0], \"
|
943
|
+
<td><%= link_to(r[0], r[0] && send(\"#\{r[0]&._brick_index}_path\".to_sym)) %></td>
|
889
944
|
<td<%= if r[1]
|
890
945
|
' class=\"orphan\"' unless ::Brick.relations.key?(r[1])
|
891
946
|
else
|
@@ -942,12 +997,15 @@ if (description = (relation = Brick.relations[#{model_name}.table_name])&.fetch(
|
|
942
997
|
end
|
943
998
|
%><%= link_to '(See all #{obj_name.pluralize})', #{@_brick_model._brick_index}_path %>
|
944
999
|
#{erd_markup}
|
945
|
-
<% if obj
|
1000
|
+
<% if obj
|
1001
|
+
# path_options = [obj.#{pk}]
|
1002
|
+
# path_options << { '_brick_schema': } if
|
1003
|
+
# url = send(:#\{model_name._brick_index(:singular)}_path, obj.#{pk})
|
1004
|
+
options = {}
|
1005
|
+
options[:url] = send(\"#\{#{model_name}._brick_index(:singular)}_path\".to_sym, obj) if ::Brick.config.path_prefix
|
1006
|
+
%>
|
946
1007
|
<br><br>
|
947
|
-
<%= #
|
948
|
-
# path_options << { '_brick_schema': } if
|
949
|
-
# url = send(:#{model_name.underscore}_path, obj.#{pk})
|
950
|
-
form_for(obj.becomes(#{model_name})) do |f| %>
|
1008
|
+
<%= form_for(obj.becomes(#{model_name}), options) do |f| %>
|
951
1009
|
<table class=\"shadow\">
|
952
1010
|
<% has_fields = false
|
953
1011
|
@#{obj_name}.attributes.each do |k, val|
|
@@ -966,7 +1024,7 @@ end
|
|
966
1024
|
bt_pair = nil
|
967
1025
|
loop do
|
968
1026
|
bt_pair = bt[1].find { |pair| pair.first.name == poly_class_name }
|
969
|
-
#
|
1027
|
+
# Accommodate any valid STI by going up the chain of inheritance
|
970
1028
|
break unless bt_pair.nil? && poly_class_name = ::Brick.existing_stis[poly_class_name]
|
971
1029
|
end
|
972
1030
|
puts \"*** Might be missing an STI class called #\{orig_poly_name\} whose base class should have this:
|
@@ -1001,7 +1059,7 @@ end
|
|
1001
1059
|
html_options[:prompt] = \"Select #\{bt_name\}\" %>
|
1002
1060
|
<%= f.select k.to_sym, bt[3], { value: val || '^^^brick_NULL^^^' }, html_options %>
|
1003
1061
|
<%= if (bt_obj = bt_class&.find_by(bt_pair[1] => val))
|
1004
|
-
link_to('⇛', send(\"#\{bt_class.base_class.
|
1062
|
+
link_to('⇛', send(\"#\{bt_class.base_class._brick_index(:singular)\}_path\".to_sym, bt_obj.send(bt_class.primary_key.to_sym)), { class: 'show-arrow' })
|
1005
1063
|
elsif val
|
1006
1064
|
\"<span class=\\\"orphan\\\">Orphaned ID: #\{val}</span>\".html_safe
|
1007
1065
|
end %>
|
@@ -1059,9 +1117,10 @@ end
|
|
1059
1117
|
<tr><td colspan=\"2\">(No displayable fields)</td></tr>
|
1060
1118
|
<% end %>
|
1061
1119
|
</table>
|
1062
|
-
|
1120
|
+
<% end %>
|
1063
1121
|
|
1064
|
-
|
1122
|
+
#{unless args.first == 'new'
|
1123
|
+
hms_headers.each_with_object(+'') do |hm, s|
|
1065
1124
|
# %%% Would be able to remove this when multiple foreign keys to same destination becomes bulletproof
|
1066
1125
|
next if hm.first.options[:through] && !hm.first.through_reflection
|
1067
1126
|
|
@@ -1076,14 +1135,15 @@ end
|
|
1076
1135
|
<tr><td>(none)</td></tr>
|
1077
1136
|
<% else %>
|
1078
1137
|
<% collection.uniq.each do |#{hm_singular_name}| %>
|
1079
|
-
<tr><td><%= link_to(#{hm_singular_name}.brick_descrip, #{hm.first.klass.
|
1138
|
+
<tr><td><%= link_to(#{hm_singular_name}.brick_descrip, #{hm.first.klass._brick_index(:singular)}_path(slashify(#{obj_pk}))) %></td></tr>
|
1080
1139
|
<% end %>
|
1081
1140
|
<% end %>
|
1082
1141
|
</table>"
|
1083
1142
|
else
|
1084
1143
|
s
|
1085
1144
|
end
|
1086
|
-
end
|
1145
|
+
end
|
1146
|
+
end}
|
1087
1147
|
<% end %>
|
1088
1148
|
#{script}"
|
1089
1149
|
|
data/lib/brick/version_number.rb
CHANGED
data/lib/brick.rb
CHANGED
@@ -216,6 +216,12 @@ module Brick
|
|
216
216
|
true
|
217
217
|
end
|
218
218
|
|
219
|
+
# Any path prefixing to apply to all auto-generated Brick routes
|
220
|
+
# @api public
|
221
|
+
def path_prefix=(path)
|
222
|
+
Brick.config.path_prefix = path
|
223
|
+
end
|
224
|
+
|
219
225
|
# Switches Brick auto-models on or off, for all threads
|
220
226
|
# @api public
|
221
227
|
def enable_models=(value)
|
@@ -329,6 +335,15 @@ module Brick
|
|
329
335
|
end
|
330
336
|
end
|
331
337
|
|
338
|
+
# Custom columns to add to a table, minimally defined with a name and DSL string.
|
339
|
+
# @api public
|
340
|
+
def custom_columns=(cust_cols)
|
341
|
+
if cust_cols
|
342
|
+
cust_cols = cust_cols.call if cust_cols.is_a?(Proc)
|
343
|
+
Brick.config.custom_columns = cust_cols
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
332
347
|
# @api public
|
333
348
|
def order=(value)
|
334
349
|
Brick.config.order = value
|
@@ -529,23 +544,40 @@ In config/initializers/brick.rb appropriate entries would look something like:
|
|
529
544
|
view_class_length = 37 # Length of "Classes that can be built from views:"
|
530
545
|
existing_controllers = routes.each_with_object({}) { |r, s| c = r.defaults[:controller]; s[c] = nil if c }
|
531
546
|
::Rails.application.routes.append do
|
547
|
+
brick_routes_create = lambda do |schema_name, controller_name, v, options|
|
548
|
+
if schema_name # && !Object.const_defined('Apartment')
|
549
|
+
send(:namespace, schema_name) do
|
550
|
+
send(:resources, v[:resource].to_sym, **options)
|
551
|
+
end
|
552
|
+
else
|
553
|
+
send(:resources, v[:resource].to_sym, **options)
|
554
|
+
end
|
555
|
+
end
|
556
|
+
|
532
557
|
# %%% TODO: If no auto-controllers then enumerate the controllers folder in order to build matching routes
|
533
558
|
# If auto-controllers and auto-models are both enabled then this makes sense:
|
534
559
|
::Brick.relations.each do |k, v|
|
535
560
|
unless !(controller_name = v.fetch(:resource, nil)&.pluralize) || existing_controllers.key?(controller_name)
|
536
561
|
options = {}
|
537
562
|
options[:only] = [:index, :show] if v.key?(:isView)
|
563
|
+
# First do the API routes
|
538
564
|
full_resource = nil
|
539
|
-
|
540
|
-
|
541
|
-
send(:resources, v[:resource].to_sym, **options)
|
542
|
-
end
|
565
|
+
controller_prefix = (::Brick.config.path_prefix ? "#{::Brick.config.path_prefix}/" : '')
|
566
|
+
if (schema_name = v.fetch(:schema, nil))
|
543
567
|
full_resource = "#{schema_name}/#{v[:resource]}"
|
544
|
-
send(:get, "#{::Brick.api_root}#{full_resource}", { to: "#{schema_name}/#{controller_name}#index" }) if Object.const_defined?('Rswag::Ui')
|
568
|
+
send(:get, "#{::Brick.api_root}#{full_resource}", { to: "#{controller_prefix}#{schema_name}/#{controller_name}#index" }) if Object.const_defined?('Rswag::Ui')
|
545
569
|
else
|
546
|
-
send(:resources, v[:resource].to_sym, **options)
|
547
570
|
# Normally goes to something like: /api/v1/employees
|
548
|
-
send(:get, "#{::Brick.api_root}#{v[:resource]}", { to: "#{controller_name}#index" }) if Object.const_defined?('Rswag::Ui')
|
571
|
+
send(:get, "#{::Brick.api_root}#{v[:resource]}", { to: "#{controller_prefix}#{controller_name}#index" }) if Object.const_defined?('Rswag::Ui')
|
572
|
+
end
|
573
|
+
# Now the normal routes
|
574
|
+
if ::Brick.config.path_prefix
|
575
|
+
# Was: send(:scope, path: ::Brick.config.path_prefix) do
|
576
|
+
send(:namespace, ::Brick.config.path_prefix) do
|
577
|
+
brick_routes_create.call(schema_name, controller_name, v, options)
|
578
|
+
end
|
579
|
+
else
|
580
|
+
brick_routes_create.call(schema_name, controller_name, v, options)
|
549
581
|
end
|
550
582
|
|
551
583
|
if (class_name = v.fetch(:class_name, nil))
|
@@ -139,6 +139,10 @@ module Brick
|
|
139
139
|
# # Settings for the Brick gem
|
140
140
|
# # (By default this auto-creates models, controllers, views, and routes on-the-fly.)
|
141
141
|
|
142
|
+
# # Custom path prefix to apply to all auto-generated Brick routes. Also causes auto-generated controllers
|
143
|
+
# # to be created inside a module with the same name.
|
144
|
+
# ::Brick.path_prefix = 'admin'
|
145
|
+
|
142
146
|
# # Normally all are enabled in development mode, and for security reasons only models are enabled in production
|
143
147
|
# # and test. This allows you to either (a) turn off models entirely, or (b) enable controllers, views, and routes
|
144
148
|
# # in production.
|
@@ -207,6 +211,12 @@ module Brick
|
|
207
211
|
# # to be the primary key.)
|
208
212
|
#{bar}
|
209
213
|
|
214
|
+
# # Custom columns to add to a table, minimally defined with a name and DSL string.
|
215
|
+
# Brick.custom_columns = { 'users' => { messages: ['[COUNT(messages)] messages', 'messages'] },
|
216
|
+
# 'orders' => { salesperson: '[salesperson.first] [salesperson.last]',
|
217
|
+
# products: ['[COUNT(order_items.product)] products', 'order_items.product' ] }
|
218
|
+
# }
|
219
|
+
|
210
220
|
# # Skip creating a has_many association for these (only retain the belongs_to built from this additional_reference).
|
211
221
|
# # (Uses the same exact three-part format as would define an additional_reference)
|
212
222
|
# # Say for instance that we didn't care to display the favourite colours that users have:
|
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.77
|
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-
|
11
|
+
date: 2022-10-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|