brick 1.0.18 → 1.0.21
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 +14 -0
- data/lib/brick/extensions.rb +317 -67
- data/lib/brick/frameworks/rails/engine.rb +81 -93
- data/lib/brick/join_array.rb +227 -0
- data/lib/brick/version_number.rb +1 -1
- data/lib/brick.rb +42 -2
- data/lib/generators/brick/install_generator.rb +16 -11
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0ad289d9f4db9ef6ce9edcfb8948926d4a61910294291272ccf2b342fa2f5051
|
4
|
+
data.tar.gz: 9030f9ccf39d7f0a9a0ff9413e4f24ee7a46beaf3aba056e9875fc5c29551419
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ed6f08eabba91cb304141a0c84884cfbeaae0087094b1fe1ff4a80195aee44dbb697c385ab9d7afaac2e337f9008bc7bef059be98f95264fec34a2288beaff6b
|
7
|
+
data.tar.gz: 5ebbb793d23634232a529b3d39224e1e4deaa010cf8bf2841effe20a9e3b4d7079fb7163e6c2f90feeed8526c4a1314f319bad47d1c8a6079bf54ca92f0e6224
|
data/lib/brick/config.rb
CHANGED
@@ -74,6 +74,20 @@ module Brick
|
|
74
74
|
@mutex.synchronize { @exclude_hms = skips }
|
75
75
|
end
|
76
76
|
|
77
|
+
# Skip showing counts for these specific has_many associations when building auto-generated #index views
|
78
|
+
def skip_index_hms
|
79
|
+
@mutex.synchronize { @skip_index_hms || {} }
|
80
|
+
end
|
81
|
+
|
82
|
+
def skip_index_hms=(skips)
|
83
|
+
@mutex.synchronize do
|
84
|
+
@skip_index_hms ||= skips.each_with_object({}) do |v, s|
|
85
|
+
class_name, assoc_name = v.split('.')
|
86
|
+
(s[class_name] ||= {})[assoc_name.to_sym] = nil
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
77
91
|
# Associations to treat as a has_one
|
78
92
|
def has_ones
|
79
93
|
@mutex.synchronize { @has_ones }
|
data/lib/brick/extensions.rb
CHANGED
@@ -32,32 +32,101 @@
|
|
32
32
|
|
33
33
|
# Currently quadrupling up routes
|
34
34
|
|
35
|
+
|
36
|
+
# From the North app:
|
37
|
+
# undefined method `built_in_role_path' when referencing show on a subclassed STI:
|
38
|
+
# http://localhost:3000/roles/3?_brick_schema=cust1
|
39
|
+
|
40
|
+
|
35
41
|
# ==========================================================
|
36
42
|
# Dynamically create model or controller classes when needed
|
37
43
|
# ==========================================================
|
38
44
|
|
39
45
|
# By default all models indicate that they are not views
|
46
|
+
module Arel
|
47
|
+
class Table
|
48
|
+
def _arel_table_type
|
49
|
+
# AR < 4.2 doesn't have type_caster at all, so rely on an instance variable getting set
|
50
|
+
# AR 4.2 - 5.1 have buggy type_caster entries for the root node
|
51
|
+
instance_variable_get(:@_arel_table_type) ||
|
52
|
+
# 5.2-7.0 does type_caster just fine, no bugs there, but the property with the type differs:
|
53
|
+
# 5.2 has "types" as public, 6.0 "types" as private, and >= 6.1 "klass" as private.
|
54
|
+
((tc = send(:type_caster)) && tc.instance_variable_get(:@types)) ||
|
55
|
+
tc.send(:klass)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
40
60
|
module ActiveRecord
|
41
61
|
class Base
|
62
|
+
def self._assoc_names
|
63
|
+
@_assoc_names ||= {}
|
64
|
+
end
|
65
|
+
|
42
66
|
def self.is_view?
|
43
67
|
false
|
44
68
|
end
|
45
69
|
|
46
70
|
# Used to show a little prettier name for an object
|
47
|
-
def
|
48
|
-
klass = self.class
|
49
|
-
# If available, parse simple DSL attached to a model in order to provide a friendlier name.
|
50
|
-
# Object property names can be referenced in square brackets like this:
|
51
|
-
# { 'User' => '[profile.firstname] [profile.lastname]' }
|
52
|
-
|
71
|
+
def self.brick_get_dsl
|
53
72
|
# If there's no DSL yet specified, just try to find the first usable column on this model
|
54
|
-
unless ::Brick.config.model_descrips[
|
55
|
-
descrip_col = (
|
73
|
+
unless (dsl = ::Brick.config.model_descrips[name])
|
74
|
+
descrip_col = (columns.map(&:name) - _brick_get_fks -
|
56
75
|
(::Brick.config.metadata_columns || []) -
|
57
|
-
[
|
58
|
-
::Brick.config.model_descrips[
|
76
|
+
[primary_key]).first
|
77
|
+
dsl = ::Brick.config.model_descrips[name] = "[#{descrip_col}]" if descrip_col
|
78
|
+
end
|
79
|
+
dsl
|
80
|
+
end
|
81
|
+
|
82
|
+
# Pass in true or a JoinArray
|
83
|
+
def self.brick_parse_dsl(build_array = nil, prefix = [], translations = {})
|
84
|
+
build_array = ::Brick::JoinArray.new.tap { |ary| ary.replace([build_array]) } if build_array.is_a?(::Brick::JoinHash)
|
85
|
+
build_array = ::Brick::JoinArray.new unless build_array.nil? || build_array.is_a?(Array)
|
86
|
+
members = []
|
87
|
+
bracket_name = nil
|
88
|
+
prefix = [prefix] unless prefix.is_a?(Array)
|
89
|
+
if (dsl = ::Brick.config.model_descrips[name] || brick_get_dsl)
|
90
|
+
klass = nil
|
91
|
+
dsl.each_char do |ch|
|
92
|
+
if bracket_name
|
93
|
+
if ch == ']' # Time to process a bracketed thing?
|
94
|
+
parts = bracket_name.split('.')
|
95
|
+
first_parts = parts[0..-2].map { |part| klass = klass.reflect_on_association(part_sym = part.to_sym).klass; part_sym }
|
96
|
+
parts = prefix + first_parts + [parts[-1]]
|
97
|
+
if parts.length > 1
|
98
|
+
s = build_array
|
99
|
+
parts[0..-3].each { |v| s = s[v.to_sym] }
|
100
|
+
s[parts[-2]] = nil # unless parts[-2].empty? # Using []= will "hydrate" any missing part(s) in our whole series
|
101
|
+
translations[parts[0..-2].join('.')] = klass
|
102
|
+
end
|
103
|
+
members << parts
|
104
|
+
bracket_name = nil
|
105
|
+
else
|
106
|
+
bracket_name << ch
|
107
|
+
end
|
108
|
+
elsif ch == '['
|
109
|
+
bracket_name = +''
|
110
|
+
klass = self
|
111
|
+
end
|
112
|
+
end
|
113
|
+
else # With no DSL available, still put this prefix into the JoinArray so we can get primary key (ID) info from this table
|
114
|
+
x = prefix.each_with_object(build_array) { |v, s| s[v.to_sym] }
|
115
|
+
x[prefix[-1]] = nil unless prefix.empty? # Using []= will "hydrate" any missing part(s) in our whole series
|
59
116
|
end
|
60
|
-
|
117
|
+
members
|
118
|
+
end
|
119
|
+
|
120
|
+
# If available, parse simple DSL attached to a model in order to provide a friendlier name.
|
121
|
+
# Object property names can be referenced in square brackets like this:
|
122
|
+
# { 'User' => '[profile.firstname] [profile.lastname]' }
|
123
|
+
def brick_descrip
|
124
|
+
self.class.brick_descrip(self)
|
125
|
+
end
|
126
|
+
|
127
|
+
def self.brick_descrip(obj, data = nil, pk_alias = nil)
|
128
|
+
if (dsl = ::Brick.config.model_descrips[(klass = self).name] || klass.brick_get_dsl)
|
129
|
+
idx = -1
|
61
130
|
caches = {}
|
62
131
|
output = +''
|
63
132
|
is_brackets_have_content = false
|
@@ -65,18 +134,23 @@ module ActiveRecord
|
|
65
134
|
dsl.each_char do |ch|
|
66
135
|
if bracket_name
|
67
136
|
if ch == ']' # Time to process a bracketed thing?
|
68
|
-
|
69
|
-
|
70
|
-
bracket_name.split('.').each do |part|
|
71
|
-
obj_name += ".#{part}"
|
72
|
-
obj = if caches.key?(obj_name)
|
73
|
-
caches[obj_name]
|
137
|
+
datum = if data
|
138
|
+
data[idx += 1].to_s
|
74
139
|
else
|
75
|
-
|
140
|
+
obj_name = +''
|
141
|
+
this_obj = obj
|
142
|
+
bracket_name.split('.').each do |part|
|
143
|
+
obj_name += ".#{part}"
|
144
|
+
this_obj = if caches.key?(obj_name)
|
145
|
+
caches[obj_name]
|
146
|
+
else
|
147
|
+
(caches[obj_name] = this_obj&.send(part.to_sym))
|
148
|
+
end
|
149
|
+
end
|
150
|
+
this_obj&.to_s || ''
|
76
151
|
end
|
77
|
-
|
78
|
-
|
79
|
-
output << (obj&.to_s || '')
|
152
|
+
is_brackets_have_content = true unless (datum).blank?
|
153
|
+
output << (datum || '')
|
80
154
|
bracket_name = nil
|
81
155
|
else
|
82
156
|
bracket_name << ch
|
@@ -91,10 +165,14 @@ module ActiveRecord
|
|
91
165
|
end
|
92
166
|
if is_brackets_have_content
|
93
167
|
output
|
94
|
-
elsif
|
95
|
-
|
168
|
+
elsif pk_alias
|
169
|
+
if (id = obj.send(pk_alias))
|
170
|
+
"#{klass.name} ##{id}"
|
171
|
+
end
|
172
|
+
# elsif klass.primary_key
|
173
|
+
# "#{klass.name} ##{obj.send(klass.primary_key)}"
|
96
174
|
else
|
97
|
-
to_s
|
175
|
+
obj.to_s
|
98
176
|
end
|
99
177
|
end
|
100
178
|
|
@@ -106,9 +184,91 @@ module ActiveRecord
|
|
106
184
|
end
|
107
185
|
|
108
186
|
class Relation
|
109
|
-
|
187
|
+
attr_reader :_brick_chains
|
188
|
+
|
189
|
+
# CLASS STUFF
|
190
|
+
def _recurse_arel(piece, prefix = '')
|
191
|
+
names = []
|
192
|
+
# Our JOINs mashup of nested arrays and hashes
|
193
|
+
# binding.pry if defined?(@arel)
|
194
|
+
case piece
|
195
|
+
when Array
|
196
|
+
names += piece.inject([]) { |s, v| s + _recurse_arel(v, prefix) }
|
197
|
+
when Hash
|
198
|
+
names += piece.inject([]) do |s, v|
|
199
|
+
new_prefix = "#{prefix}#{v.first}_"
|
200
|
+
s << [v.last.shift, new_prefix]
|
201
|
+
s + _recurse_arel(v.last, new_prefix)
|
202
|
+
end
|
203
|
+
|
204
|
+
# ActiveRecord AREL objects
|
205
|
+
when Arel::Nodes::Join # INNER or OUTER JOIN
|
206
|
+
# rubocop:disable Style/IdenticalConditionalBranches
|
207
|
+
if piece.right.is_a?(Arel::Table) # Came in from AR < 3.2?
|
208
|
+
# Arel 2.x and older is a little curious because these JOINs work "back to front".
|
209
|
+
# The left side here is either another earlier JOIN, or at the end of the whole tree, it is
|
210
|
+
# the first table.
|
211
|
+
names += _recurse_arel(piece.left)
|
212
|
+
# The right side here at the top is the very last table, and anywhere else down the tree it is
|
213
|
+
# the later "JOIN" table of this pair. (The table that comes after all the rest of the JOINs
|
214
|
+
# from the left side.)
|
215
|
+
names << [piece.right._arel_table_type, (piece.right.table_alias || piece.right.name)]
|
216
|
+
else # "Normal" setup, fed from a JoinSource which has an array of JOINs
|
217
|
+
# The left side is the "JOIN" table
|
218
|
+
names += _recurse_arel(piece.left)
|
219
|
+
# The expression on the right side is the "ON" clause
|
220
|
+
# on = piece.right.expr
|
221
|
+
# # Find the table which is not ourselves, and thus must be the "path" that led us here
|
222
|
+
# parent = piece.left == on.left.relation ? on.right.relation : on.left.relation
|
223
|
+
# binding.pry if piece.left.is_a?(Arel::Nodes::TableAlias)
|
224
|
+
table = piece.left
|
225
|
+
if table.is_a?(Arel::Nodes::TableAlias)
|
226
|
+
alias_name = table.right
|
227
|
+
table = table.left
|
228
|
+
end
|
229
|
+
(_brick_chains[table._arel_table_type] ||= []) << (alias_name || table.table_alias || table.name)
|
230
|
+
# puts "YES! #{self.object_id}"
|
231
|
+
end
|
232
|
+
# rubocop:enable Style/IdenticalConditionalBranches
|
233
|
+
when Arel::Table # Table
|
234
|
+
names << [piece._arel_table_type, (piece.table_alias || piece.name)]
|
235
|
+
when Arel::Nodes::TableAlias # Alias
|
236
|
+
# Can get the real table name from: self._recurse_arel(piece.left)
|
237
|
+
names << [piece.left._arel_table_type, piece.right.to_s] # This is simply a string; the alias name itself
|
238
|
+
when Arel::Nodes::JoinSource # Leaving this until the end because AR < 3.2 doesn't know at all about JoinSource!
|
239
|
+
# Spin up an empty set of Brick alias name chains at the start
|
240
|
+
@_brick_chains = {}
|
241
|
+
# The left side is the "FROM" table
|
242
|
+
# names += _recurse_arel(piece.left)
|
243
|
+
names << [piece.left._arel_table_type, (piece.left.table_alias || piece.left.name)]
|
244
|
+
# The right side is an array of all JOINs
|
245
|
+
piece.right.each { |join| names << _recurse_arel(join) }
|
246
|
+
end
|
247
|
+
names
|
248
|
+
end
|
249
|
+
|
250
|
+
# INSTANCE STUFF
|
251
|
+
def _arel_alias_names
|
252
|
+
# %%% If with Rails 3.1 and older you get "NoMethodError: undefined method `eq' for nil:NilClass"
|
253
|
+
# when trying to call relation.arel, then somewhere along the line while navigating a has_many
|
254
|
+
# relationship it can't find the proper foreign key.
|
255
|
+
core = arel.ast.cores.first
|
256
|
+
# Accommodate AR < 3.2
|
257
|
+
if core.froms.is_a?(Arel::Table)
|
258
|
+
# All recent versions of AR have #source which brings up an Arel::Nodes::JoinSource
|
259
|
+
_recurse_arel(core.source)
|
260
|
+
else
|
261
|
+
# With AR < 3.2, "froms" brings up the top node, an Arel::Nodes::InnerJoin
|
262
|
+
_recurse_arel(core.froms)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
def brick_select(params, selects = nil, bt_descrip = {}, hm_counts = {}, join_array = ::Brick::JoinArray.new
|
267
|
+
# , is_add_bts, is_add_hms
|
268
|
+
)
|
269
|
+
is_add_bts = is_add_hms = true
|
110
270
|
wheres = {}
|
111
|
-
|
271
|
+
has_hm = false
|
112
272
|
params.each do |k, v|
|
113
273
|
case (ks = k.split('.')).length
|
114
274
|
when 1
|
@@ -116,28 +276,93 @@ module ActiveRecord
|
|
116
276
|
when 2
|
117
277
|
assoc_name = ks.first.to_sym
|
118
278
|
# Make sure it's a good association name and that the model has that column name
|
119
|
-
next unless klass.reflect_on_association(assoc_name)&.klass&.columns&.map(&:name)&.include?(ks.last)
|
120
|
-
|
121
|
-
|
279
|
+
next unless (assoc = klass.reflect_on_association(assoc_name))&.klass&.columns&.map(&:name)&.include?(ks.last)
|
280
|
+
|
281
|
+
# So that we can map an association name to any special alias name used in an AREL query
|
282
|
+
ans = (assoc.klass._assoc_names[assoc_name] ||= [])
|
283
|
+
ans << assoc.klass unless ans.include?(assoc.klass)
|
284
|
+
# There is some potential for duplicates when there is an HM-based where in play. De-duplicate if so.
|
285
|
+
has_hm ||= assoc.macro == :has_many
|
286
|
+
join_array[assoc_name] = nil # Store this relation name in our special collection for .joins()
|
122
287
|
end
|
123
288
|
wheres[k] = v.split(',')
|
124
289
|
end
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
290
|
+
# distinct! if has_hm
|
291
|
+
|
292
|
+
# %%% Skip the metadata columns
|
293
|
+
if selects&.empty? # Default to all columns
|
294
|
+
columns.each do |col|
|
295
|
+
selects << "#{table.name}.#{col.name}"
|
296
|
+
end
|
129
297
|
end
|
298
|
+
|
299
|
+
# Search for BT, HM, and HMT DSL stuff
|
300
|
+
translations = {}
|
301
|
+
if is_add_bts || is_add_hms
|
302
|
+
bts, hms, associatives = ::Brick.get_bts_and_hms(klass)
|
303
|
+
bts.each do |_k, bt|
|
304
|
+
# join_array[bt.first] = nil # Store this relation name in our special collection for .joins()
|
305
|
+
bt_descrip[bt.first] = [bt.last, bt.last.brick_parse_dsl(join_array, bt.first, translations)]
|
306
|
+
end
|
307
|
+
skip_klass_hms = ::Brick.config.skip_index_hms[klass.name] || {}
|
308
|
+
hms.each do |k, hm|
|
309
|
+
next if skip_klass_hms.key?(k)
|
310
|
+
|
311
|
+
join_array[k] = nil # Store this relation name in our special collection for .joins()
|
312
|
+
hm_counts[k] = nil # Placeholder that will be filled in once we know the proper table alias
|
313
|
+
end
|
314
|
+
end
|
315
|
+
where!(wheres) unless wheres.empty?
|
316
|
+
if join_array.present?
|
317
|
+
left_outer_joins!(join_array) # joins!(join_array)
|
318
|
+
# Without working from a duplicate, touching the AREL ast tree sets the @arel instance variable, which causes the relation to be immutable.
|
319
|
+
(rel_dupe = dup)._arel_alias_names
|
320
|
+
core_selects = selects.dup
|
321
|
+
groups = []
|
322
|
+
chains = rel_dupe._brick_chains
|
323
|
+
id_for_tables = {}
|
324
|
+
bt_columns = bt_descrip.each_with_object([]) do |v, s|
|
325
|
+
tbl_name = chains[v.last.first].first
|
326
|
+
if (id_col = v.last.first.primary_key) && !id_for_tables.key?(tbl_name)
|
327
|
+
groups << (unaliased = "#{tbl_name}.#{id_col}")
|
328
|
+
selects << "#{unaliased} AS \"#{(id_alias = id_for_tables[tbl_name] = "_brfk_#{v.first}__#{id_col}")}\""
|
329
|
+
v.last << id_alias
|
330
|
+
end
|
331
|
+
if (col_name = v.last[1].last&.last)
|
332
|
+
v.last[1].map { |x| [translations[x[0..-2].map(&:to_s).join('.')], x.last] }.each_with_index do |sel_col, idx|
|
333
|
+
groups << (unaliased = "#{tbl_name = chains[sel_col.first].first}.#{sel_col.last}")
|
334
|
+
# col_name is weak when there are multiple, using sel_col.last instead
|
335
|
+
tbl_name2 = tbl_name.start_with?('public.') ? tbl_name[7..-1] : tbl_name
|
336
|
+
selects << "#{unaliased} AS \"#{(col_alias = "_brfk_#{tbl_name2}__#{sel_col.last}")}\""
|
337
|
+
v.last[1][idx] << col_alias
|
338
|
+
end
|
339
|
+
end
|
340
|
+
end
|
341
|
+
group!(core_selects + groups) if hm_counts.any? # + bt_columns
|
342
|
+
join_array.each do |assoc_name|
|
343
|
+
# %%% Need to support {user: :profile}
|
344
|
+
next unless assoc_name.is_a?(Symbol)
|
345
|
+
|
346
|
+
klass = reflect_on_association(assoc_name)&.klass
|
347
|
+
table_alias = chains[klass].length > 1 ? chains[klass].shift : chains[klass].first
|
348
|
+
_assoc_names[assoc_name] = [table_alias, klass]
|
349
|
+
end
|
350
|
+
# Copy entries over
|
351
|
+
hm_counts.keys.each do |k|
|
352
|
+
hm_counts[k] = _assoc_names[k]
|
353
|
+
end
|
354
|
+
end
|
355
|
+
wheres unless wheres.empty? # Return the specific parameters that we did use
|
130
356
|
end
|
131
357
|
end
|
132
358
|
|
133
359
|
module Inheritance
|
134
360
|
module ClassMethods
|
135
|
-
|
361
|
+
private
|
136
362
|
|
137
363
|
alias _brick_find_sti_class find_sti_class
|
138
364
|
def find_sti_class(type_name)
|
139
365
|
if ::Brick.sti_models.key?(type_name)
|
140
|
-
# puts ['X', self.name, type_name].inspect
|
141
366
|
_brick_find_sti_class(type_name)
|
142
367
|
else
|
143
368
|
# This auto-STI is more of a brute-force approach, building modules where needed
|
@@ -146,10 +371,8 @@ module ActiveRecord
|
|
146
371
|
module_prefixes = type_name.split('::')
|
147
372
|
module_prefixes.unshift('') unless module_prefixes.first.blank?
|
148
373
|
module_name = module_prefixes[0..-2].join('::')
|
149
|
-
if ::Brick.config.sti_namespace_prefixes&.key?("::#{module_name}::") ||
|
150
|
-
|
151
|
-
_brick_find_sti_class(type_name)
|
152
|
-
elsif File.exists?(candidate_file = Rails.root.join('app/models' + module_prefixes.map(&:underscore).join('/') + '.rb'))
|
374
|
+
if (snp = ::Brick.config.sti_namespace_prefixes)&.key?("::#{module_name}::") || snp&.key?("#{module_name}::") ||
|
375
|
+
File.exist?(candidate_file = Rails.root.join('app/models' + module_prefixes.map(&:underscore).join('/') + '.rb'))
|
153
376
|
_brick_find_sti_class(type_name) # Find this STI class normally
|
154
377
|
else
|
155
378
|
# Build missing prefix modules if they don't yet exist
|
@@ -228,10 +451,9 @@ class Object
|
|
228
451
|
end
|
229
452
|
|
230
453
|
relations = ::Brick.instance_variable_get(:@relations)[ActiveRecord::Base.connection_pool.object_id] || {}
|
231
|
-
|
232
|
-
result = if is_controllers_enabled && class_name.end_with?('Controller') && (plural_class_name = class_name[0..-11]).length.positive?
|
454
|
+
result = if ::Brick.enable_controllers? && class_name.end_with?('Controller') && (plural_class_name = class_name[0..-11]).length.positive?
|
233
455
|
# Otherwise now it's up to us to fill in the gaps
|
234
|
-
if (model =
|
456
|
+
if (model = plural_class_name.singularize.constantize)
|
235
457
|
# if it's a controller and no match or a model doesn't really use the same table name, eager load all models and try to find a model class of the right name.
|
236
458
|
build_controller(class_name, plural_class_name, model, relations)
|
237
459
|
end
|
@@ -243,10 +465,10 @@ class Object
|
|
243
465
|
|
244
466
|
# Adjust for STI if we know of a base model for the requested model name
|
245
467
|
table_name = if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, nil))
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
468
|
+
base_model.table_name
|
469
|
+
else
|
470
|
+
ActiveSupport::Inflector.pluralize(singular_table_name)
|
471
|
+
end
|
250
472
|
|
251
473
|
# Maybe, just maybe there's a database table that will satisfy this need
|
252
474
|
if (matching = [table_name, singular_table_name, plural_class_name, model_name].find { |m| relations.key?(m) })
|
@@ -282,6 +504,7 @@ class Object
|
|
282
504
|
end
|
283
505
|
return
|
284
506
|
end
|
507
|
+
|
285
508
|
if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, nil))
|
286
509
|
is_sti = true
|
287
510
|
else
|
@@ -443,13 +666,21 @@ class Object
|
|
443
666
|
|
444
667
|
code << " def index\n"
|
445
668
|
code << " @#{table_name} = #{model.name}#{model.primary_key ? ".order(#{model.primary_key.inspect})" : '.all'}\n"
|
446
|
-
code << " @#{table_name}.
|
669
|
+
code << " @#{table_name}.brick_select(params)\n"
|
447
670
|
code << " end\n"
|
448
671
|
self.define_method :index do
|
449
672
|
::Brick.set_db_schema(params)
|
450
|
-
ar_relation = model.primary_key ? model.order(model.primary_key) : model.all
|
451
|
-
|
452
|
-
|
673
|
+
ar_relation = model.all # model.primary_key ? model.order(model.primary_key) : model.all
|
674
|
+
@_brick_params = ar_relation.brick_select(params, (selects = []), (bt_descrip = {}), (hm_counts = {}), (join_array = ::Brick::JoinArray.new))
|
675
|
+
# %%% Add custom HM count columns
|
676
|
+
# %%% What happens when the PK is composite?
|
677
|
+
counts = hm_counts.each_with_object([]) { |v, s| s << "COUNT(DISTINCT #{v.last.first}.#{v.last.last.primary_key}) AS _br_#{v.first}_ct" }
|
678
|
+
# *selects,
|
679
|
+
instance_variable_set("@#{table_name}".to_sym, ar_relation.dup._select!(*selects, *counts))
|
680
|
+
# binding.pry
|
681
|
+
@_brick_bt_descrip = bt_descrip
|
682
|
+
@_brick_hm_counts = hm_counts
|
683
|
+
@_brick_join_array = join_array
|
453
684
|
end
|
454
685
|
|
455
686
|
if model.primary_key
|
@@ -522,9 +753,9 @@ module ActiveRecord::ConnectionHandling
|
|
522
753
|
end
|
523
754
|
|
524
755
|
def _brick_reflect_tables
|
525
|
-
|
526
|
-
|
527
|
-
|
756
|
+
if (relations = ::Brick.relations).empty?
|
757
|
+
# Only for Postgres? (Doesn't work in sqlite3)
|
758
|
+
# puts ActiveRecord::Base.connection.execute("SELECT current_setting('SEARCH_PATH')").to_a.inspect
|
528
759
|
|
529
760
|
schema_sql = 'SELECT NULL AS table_schema;'
|
530
761
|
case ActiveRecord::Base.connection.adapter_name
|
@@ -614,6 +845,25 @@ module ActiveRecord::ConnectionHandling
|
|
614
845
|
end
|
615
846
|
end
|
616
847
|
|
848
|
+
# # Add unique OIDs
|
849
|
+
# if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
|
850
|
+
# ActiveRecord::Base.execute_sql(
|
851
|
+
# "SELECT c.oid, n.nspname, c.relname
|
852
|
+
# FROM pg_catalog.pg_namespace AS n
|
853
|
+
# INNER JOIN pg_catalog.pg_class AS c ON n.oid = c.relnamespace
|
854
|
+
# WHERE c.relkind IN ('r', 'v')"
|
855
|
+
# ).each do |r|
|
856
|
+
# next if ['pg_catalog', 'information_schema', ''].include?(r['nspname']) ||
|
857
|
+
# ['ar_internal_metadata', 'schema_migrations'].include?(r['relname'])
|
858
|
+
# relation = relations.fetch(r['relname'], nil)
|
859
|
+
# if relation
|
860
|
+
# (relation[:oid] ||= {})[r['nspname']] = r['oid']
|
861
|
+
# else
|
862
|
+
# puts "Where is #{r['nspname']} #{r['relname']} ?"
|
863
|
+
# end
|
864
|
+
# end
|
865
|
+
# end
|
866
|
+
|
617
867
|
case ActiveRecord::Base.connection.adapter_name
|
618
868
|
when 'PostgreSQL', 'Mysql2'
|
619
869
|
sql = ActiveRecord::Base.send(:sanitize_sql_array, [
|
@@ -706,7 +956,7 @@ module Brick
|
|
706
956
|
missing << fk[0] unless relations.key?(fk[0])
|
707
957
|
missing << primary_table unless is_class || relations.key?(primary_table)
|
708
958
|
unless missing.empty?
|
709
|
-
tables = relations.reject { |
|
959
|
+
tables = relations.reject { |_k, v| v.fetch(:isView, nil) }.keys.sort
|
710
960
|
puts "Brick: Additional reference #{fk.inspect} refers to non-existent #{'table'.pluralize(missing.length)} #{missing.join(' and ')}. (Available tables include #{tables.join(', ')}.)"
|
711
961
|
return
|
712
962
|
end
|
@@ -715,7 +965,7 @@ module Brick
|
|
715
965
|
puts "Brick: Additional reference #{fk.inspect} refers to non-existent column #{fk[1]}. (Columns present in #{fk[0]} are #{columns.join(', ')}.)"
|
716
966
|
return
|
717
967
|
end
|
718
|
-
if (redundant = bts.find { |
|
968
|
+
if (redundant = bts.find { |_k, v| v[:inverse]&.fetch(:inverse_table, nil) == fk[0] && v[:fk] == fk[1] && v[:inverse_table] == primary_table })
|
719
969
|
if is_class && !redundant.last.key?(:class)
|
720
970
|
redundant.last[:primary_class] = primary_class # Round out this BT so it can find the proper :source for a HMT association that references an STI subclass
|
721
971
|
else
|
@@ -737,19 +987,19 @@ module Brick
|
|
737
987
|
# assoc_bt[:inverse_of] = primary_class.reflect_on_all_associations.find { |a| a.foreign_key == bt[1] }
|
738
988
|
end
|
739
989
|
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
assoc_bt[:inverse] = assoc_hm
|
990
|
+
return if is_class || ::Brick.config.exclude_hms&.any? { |exclusion| fk[0] == exclusion[0] && fk[1] == exclusion[1] && primary_table == exclusion[2] }
|
991
|
+
|
992
|
+
cnstr_name = "hm_#{cnstr_name}"
|
993
|
+
if (assoc_hm = hms.fetch(cnstr_name, nil))
|
994
|
+
assoc_hm[:fk] = assoc_hm[:fk].is_a?(String) ? [assoc_hm[:fk], fk[1]] : assoc_hm[:fk].concat(fk[1])
|
995
|
+
assoc_hm[:alternate_name] = "#{assoc_hm[:alternate_name]}_#{bt_assoc_name}" unless assoc_hm[:alternate_name] == bt_assoc_name
|
996
|
+
assoc_hm[:inverse] = assoc_bt
|
997
|
+
else
|
998
|
+
assoc_hm = hms[cnstr_name] = { is_bt: false, fk: fk[1], assoc_name: fk[0], alternate_name: bt_assoc_name, inverse_table: fk[0], inverse: assoc_bt }
|
999
|
+
hm_counts = relation.fetch(:hm_counts) { relation[:hm_counts] = {} }
|
1000
|
+
hm_counts[fk[0]] = hm_counts.fetch(fk[0]) { 0 } + 1
|
752
1001
|
end
|
1002
|
+
assoc_bt[:inverse] = assoc_hm
|
753
1003
|
# hms[cnstr_name] << { is_bt: false, fk: fk[1], assoc_name: fk[0], alternate_name: bt_assoc_name, inverse_table: fk[0] }
|
754
1004
|
end
|
755
1005
|
end
|
@@ -7,10 +7,12 @@ module Brick
|
|
7
7
|
# paths['app/models'] << 'lib/brick/frameworks/active_record/models'
|
8
8
|
config.brick = ActiveSupport::OrderedOptions.new
|
9
9
|
ActiveSupport.on_load(:before_initialize) do |app|
|
10
|
+
is_development = (ENV['RAILS_ENV'] || ENV['RACK_ENV']) == 'development'
|
10
11
|
::Brick.enable_models = app.config.brick.fetch(:enable_models, true)
|
11
|
-
::Brick.enable_controllers = app.config.brick.fetch(:enable_controllers,
|
12
|
-
::Brick.
|
13
|
-
::Brick.
|
12
|
+
::Brick.enable_controllers = app.config.brick.fetch(:enable_controllers, is_development)
|
13
|
+
require 'brick/join_array' if ::Brick.enable_controllers?
|
14
|
+
::Brick.enable_views = app.config.brick.fetch(:enable_views, is_development)
|
15
|
+
::Brick.enable_routes = app.config.brick.fetch(:enable_routes, is_development)
|
14
16
|
::Brick.skip_database_views = app.config.brick.fetch(:skip_database_views, false)
|
15
17
|
|
16
18
|
# Specific database tables and views to omit when auto-creating models
|
@@ -43,7 +45,7 @@ module Brick
|
|
43
45
|
# ====================================
|
44
46
|
# Dynamically create generic templates
|
45
47
|
# ====================================
|
46
|
-
if ::Brick.enable_views?
|
48
|
+
if ::Brick.enable_views?
|
47
49
|
ActionView::LookupContext.class_exec do
|
48
50
|
alias :_brick_template_exists? :template_exists?
|
49
51
|
def template_exists?(*args, **options)
|
@@ -51,14 +53,12 @@ module Brick
|
|
51
53
|
# Need to return true if we can fill in the blanks for a missing one
|
52
54
|
# args will be something like: ["index", ["categories"]]
|
53
55
|
model = args[1].map(&:camelize).join('::').singularize.constantize
|
54
|
-
if (
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
)
|
61
|
-
instance_variable_set(:@_brick_model, model)
|
56
|
+
if is_template_exists = model && (
|
57
|
+
['index', 'show'].include?(args.first) || # Everything has index and show
|
58
|
+
# Only CUD stuff has create / update / destroy
|
59
|
+
(!model.is_view? && ['new', 'create', 'edit', 'update', 'destroy'].include?(args.first))
|
60
|
+
)
|
61
|
+
@_brick_model = model
|
62
62
|
end
|
63
63
|
end
|
64
64
|
is_template_exists
|
@@ -66,56 +66,50 @@ module Brick
|
|
66
66
|
|
67
67
|
alias :_brick_find_template :find_template
|
68
68
|
def find_template(*args, **options)
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
69
|
+
return _brick_find_template(*args, **options) unless @_brick_model
|
70
|
+
|
71
|
+
model_name = @_brick_model.name
|
72
|
+
pk = @_brick_model.primary_key
|
73
|
+
obj_name = model_name.underscore
|
74
|
+
table_name = model_name.pluralize.underscore
|
75
|
+
bts, hms, associatives = ::Brick.get_bts_and_hms(@_brick_model) # This gets BT and HM and also has_many :through (HMT)
|
76
|
+
hms_columns = +'' # Used for 'index'
|
77
|
+
skip_klass_hms = ::Brick.config.skip_index_hms[model_name] || {}
|
78
|
+
hms_headers = hms.each_with_object([]) do |hm, s|
|
79
|
+
hm_assoc = hm.last
|
80
|
+
if args.first == 'index'
|
81
|
+
hm_fk_name = if hm_assoc.options[:through]
|
82
|
+
associative = associatives[hm_assoc.name]
|
83
|
+
"'#{associative.name}.#{associative.foreign_key}'"
|
84
|
+
else
|
85
|
+
hm_assoc.foreign_key
|
86
|
+
end
|
87
|
+
hms_columns << if hm_assoc.macro == :has_many
|
88
|
+
set_ct = if skip_klass_hms.key?((assoc_name = hm.first).to_sym)
|
89
|
+
'nil'
|
90
|
+
else
|
91
|
+
"#{obj_name}._br_#{assoc_name}_ct"
|
92
|
+
end
|
90
93
|
|
91
|
-
schema_options = ::Brick.db_schemas.each_with_object(+'') { |v, s| s << "<option value=\"#{v}\">#{v}</option>" }.html_safe
|
92
|
-
table_options = (::Brick.relations.keys - ::Brick.config.exclude_tables)
|
93
|
-
.each_with_object(+'') { |v, s| s << "<option value=\"#{v}\">#{v}</option>" }.html_safe
|
94
|
-
hms_columns = +'' # Used for 'index'
|
95
|
-
hms_headers = hms.each_with_object([]) do |hm, s|
|
96
|
-
next if exclude_hms.key?((hm_assoc = hm.last).name)
|
97
|
-
|
98
|
-
if args.first == 'index'
|
99
|
-
hm_fk_name = if hm_assoc.options[:through]
|
100
|
-
associative = associatives[hm_assoc.name]
|
101
|
-
"'#{associative.name}.#{associative.foreign_key}'"
|
102
|
-
else
|
103
|
-
hm_assoc.foreign_key
|
104
|
-
end
|
105
|
-
hms_columns << if hm_assoc.macro == :has_many
|
106
94
|
"<td>
|
107
|
-
<%=
|
95
|
+
<%= ct = #{set_ct}
|
96
|
+
link_to \"#\{ct || 'View'\} #{assoc_name}\", #{hm_assoc.klass.name.underscore.pluralize}_path({ #{hm_fk_name}: #{obj_name}.#{pk} }) unless ct&.zero? %>
|
108
97
|
</td>\n"
|
109
|
-
|
98
|
+
else # has_one
|
110
99
|
"<td>
|
111
100
|
<%= obj = #{obj_name}.#{hm.first}; link_to(obj.brick_descrip, obj) if obj %>
|
112
101
|
</td>\n"
|
113
|
-
|
114
|
-
end
|
115
|
-
s << [hm_assoc, "H#{hm_assoc.macro == :has_one ? 'O' : 'M'}#{'T' if hm_assoc.options[:through]} #{hm.first}"]
|
102
|
+
end
|
116
103
|
end
|
104
|
+
s << [hm_assoc, "H#{hm_assoc.macro == :has_one ? 'O' : 'M'}#{'T' if hm_assoc.options[:through]} #{hm.first}"]
|
105
|
+
end
|
117
106
|
|
118
|
-
|
107
|
+
schema_options = ::Brick.db_schemas.each_with_object(+'') { |v, s| s << "<option value=\"#{v}\">#{v}</option>" }.html_safe
|
108
|
+
# %%% If we are not auto-creating controllers (or routes) then omit by default, and if enabled anyway, such as in a development
|
109
|
+
# environment or whatever, then get either the controllers or routes list instead
|
110
|
+
table_options = (::Brick.relations.keys - ::Brick.config.exclude_tables)
|
111
|
+
.each_with_object(+'') { |v, s| s << "<option value=\"#{v.underscore.pluralize}\">#{v}</option>" }.html_safe
|
112
|
+
css = +"<style>
|
119
113
|
table {
|
120
114
|
border-collapse: collapse;
|
121
115
|
margin: 25px 0;
|
@@ -189,7 +183,12 @@ def hide_bcrypt(val)
|
|
189
183
|
is_bcrypt?(val) ? '(hidden)' : val
|
190
184
|
end %>"
|
191
185
|
|
192
|
-
|
186
|
+
if ['index', 'show', 'update'].include?(args.first)
|
187
|
+
css << "<% bts = { #{bts.each_with_object([]) { |v, s| s << "#{v.first.inspect} => [#{v.last.first.inspect}, #{v.last[1].name}, #{v.last[1].primary_key.inspect}]"}.join(', ')} } %>"
|
188
|
+
end
|
189
|
+
|
190
|
+
# %%% When doing schema select, if there's an ID then remove it, or if we're on a new page go to index
|
191
|
+
script = "<script>
|
193
192
|
var schemaSelect = document.getElementById(\"schema\");
|
194
193
|
var brickSchema;
|
195
194
|
if (schemaSelect) {
|
@@ -243,9 +242,8 @@ function changeout(href, param, value) {
|
|
243
242
|
return hrefParts[0] + \"?\" + Object.keys(params).reduce(function (s, v) { s.push(v + \"=\" + params[v]); return s; }, []).join(\"&\");
|
244
243
|
}
|
245
244
|
</script>"
|
246
|
-
|
247
|
-
|
248
|
-
when 'index'
|
245
|
+
inline = case args.first
|
246
|
+
when 'index'
|
249
247
|
"#{css}
|
250
248
|
<p style=\"color: green\"><%= notice %></p>#{"
|
251
249
|
<select id=\"schema\">#{schema_options}</select>" if ::Brick.db_schemas.length > 1}
|
@@ -253,9 +251,8 @@ function changeout(href, param, value) {
|
|
253
251
|
<h1>#{model_name.pluralize}</h1>
|
254
252
|
<% if @_brick_params&.present? %><h3>where <%= @_brick_params.each_with_object([]) { |v, s| s << \"#\{v.first\} = #\{v.last.inspect\}\" }.join(', ') %></h3><% end %>
|
255
253
|
<table id=\"#{table_name}\">
|
256
|
-
<thead><tr>#{
|
257
|
-
<%
|
258
|
-
@#{table_name}.columns.map(&:name).each do |col| %>
|
254
|
+
<thead><tr>#{'<th></th>' if pk}
|
255
|
+
<% @#{table_name}.columns.map(&:name).each do |col| %>
|
259
256
|
<% next if col == '#{pk}' || ::Brick.config.metadata_columns.include?(col) %>
|
260
257
|
<th>
|
261
258
|
<% if (bt = bts[col]) %>
|
@@ -271,16 +268,16 @@ function changeout(href, param, value) {
|
|
271
268
|
<tbody>
|
272
269
|
<% @#{table_name}.each do |#{obj_name}| %>
|
273
270
|
<tr>#{"
|
274
|
-
<td><%= link_to '⇛', #{obj_name}_path(#{obj_name}.#{pk}), { class: 'big-arrow' } %></td>" if pk
|
271
|
+
<td><%= link_to '⇛', #{obj_name}_path(#{obj_name}.#{pk}), { class: 'big-arrow' } %></td>" if pk}
|
275
272
|
<% #{obj_name}.attributes.each do |k, val| %>
|
276
|
-
<% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) %>
|
273
|
+
<% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) || k.start_with?('_brfk_') || (k.start_with?('_br_') && k.end_with?('_ct')) %>
|
277
274
|
<td>
|
278
275
|
<% if (bt = bts[k]) %>
|
279
|
-
<%#
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
276
|
+
<%# binding.pry if bt.first == :user %>
|
277
|
+
<% bt_txt = bt[1].brick_descrip(#{obj_name}, @_brick_bt_descrip[bt.first][1].map { |z| #{obj_name}.send(z.last) }, @_brick_bt_descrip[bt.first][2]) %>
|
278
|
+
<% bt_id_col = @_brick_bt_descrip[bt.first][2]; bt_id = #{obj_name}.send(bt_id_col) if bt_id_col %>
|
279
|
+
<%= bt_id ? link_to(bt_txt, send(\"#\{bt_obj_path_base = bt[1].name.underscore\}_path\".to_sym, bt_id)) : bt_txt %>
|
280
|
+
<%#= Previously was: bt_obj = bt[1].find_by(bt[2] => val); link_to(bt_obj.brick_descrip, send(\"#\{bt_obj_path_base = bt[1].name.underscore\}_path\".to_sym, bt_obj.send(bt[1].primary_key.to_sym))) if bt_obj %>
|
284
281
|
<% else %>
|
285
282
|
<%= hide_bcrypt(val) %>
|
286
283
|
<% end %>
|
@@ -295,7 +292,7 @@ function changeout(href, param, value) {
|
|
295
292
|
|
296
293
|
#{"<hr><%= link_to \"New #{obj_name}\", new_#{obj_name}_path %>" unless @_brick_model.is_view?}
|
297
294
|
#{script}"
|
298
|
-
|
295
|
+
when 'show', 'update'
|
299
296
|
"#{css}
|
300
297
|
<p style=\"color: green\"><%= notice %></p>#{"
|
301
298
|
<select id=\"schema\">#{schema_options}</select>" if ::Brick.db_schemas.length > 1}
|
@@ -308,8 +305,7 @@ function changeout(href, param, value) {
|
|
308
305
|
# url = send(:#{model_name.underscore}_path, obj.#{pk})
|
309
306
|
form_for(obj) do |f| %>
|
310
307
|
<table>
|
311
|
-
<%
|
312
|
-
@#{obj_name}.first.attributes.each do |k, val| %>
|
308
|
+
<% @#{obj_name}.first.attributes.each do |k, val| %>
|
313
309
|
<tr>
|
314
310
|
<% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) %>
|
315
311
|
<th class=\"show-field\">
|
@@ -331,7 +327,7 @@ function changeout(href, param, value) {
|
|
331
327
|
html_options = { prompt: \"Select #\{bt_name\}\" }
|
332
328
|
html_options[:class] = 'dimmed' unless val %>
|
333
329
|
<%= f.select k.to_sym, bt[3], { value: val || '^^^brick_NULL^^^' }, html_options %>
|
334
|
-
<%= bt_obj = bt[1].find_by(bt[2] => val); link_to('⇛', send(\"#\{
|
330
|
+
<%= bt_obj = bt[1].find_by(bt[2] => val); link_to('⇛', send(\"#\{bt_obj_path_base = bt_name.underscore\}_path\".to_sym, bt_obj.send(bt[1].primary_key.to_sym)), { class: 'show-arrow' }) if bt_obj %>
|
335
331
|
<% else case #{model_name}.column_for_attribute(k).type
|
336
332
|
when :string, :text %>
|
337
333
|
<% if is_bcrypt?(val) # || .readonly? %>
|
@@ -341,8 +337,13 @@ function changeout(href, param, value) {
|
|
341
337
|
<% end %>
|
342
338
|
<% when :boolean %>
|
343
339
|
<%= f.check_box k.to_sym %>
|
344
|
-
<% when :integer, :date, :datetime, :
|
340
|
+
<% when :integer, :decimal, :float, :date, :datetime, :time, :timestamp
|
341
|
+
# What happens when keys are UUID?
|
342
|
+
# Postgres naturally uses the +uuid_generate_v4()+ function from the uuid-ossp extension
|
343
|
+
# If it's not yet enabled then: enable_extension 'uuid-ossp'
|
344
|
+
# ActiveUUID gem created a new :uuid type %>
|
345
345
|
<%= val %>
|
346
|
+
<% when :binary, :primary_key %>
|
346
347
|
<% end %>
|
347
348
|
<% end %>
|
348
349
|
</td>
|
@@ -354,6 +355,7 @@ function changeout(href, param, value) {
|
|
354
355
|
|
355
356
|
#{hms_headers.map do |hm|
|
356
357
|
next unless (pk = hm.first.klass.primary_key)
|
358
|
+
|
357
359
|
"<table id=\"#{hm_name = hm.first.name.to_s}\">
|
358
360
|
<tr><th>#{hm.last}</th></tr>
|
359
361
|
<% collection = @#{obj_name}.first.#{hm_name}
|
@@ -369,19 +371,16 @@ function changeout(href, param, value) {
|
|
369
371
|
<% end %>
|
370
372
|
#{script}"
|
371
373
|
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
else
|
378
|
-
_brick_find_template(*args, **options)
|
379
|
-
end
|
374
|
+
end
|
375
|
+
# As if it were an inline template (see #determine_template in actionview-5.2.6.2/lib/action_view/renderer/template_renderer.rb)
|
376
|
+
keys = options.has_key?(:locals) ? options[:locals].keys : []
|
377
|
+
handler = ActionView::Template.handler_for_extension(options[:type] || 'erb')
|
378
|
+
ActionView::Template.new(inline, "auto-generated #{args.first} template", handler, locals: keys)
|
380
379
|
end
|
381
380
|
end
|
382
381
|
end
|
383
382
|
|
384
|
-
if ::Brick.enable_routes?
|
383
|
+
if ::Brick.enable_routes?
|
385
384
|
ActionDispatch::Routing::RouteSet.class_exec do
|
386
385
|
# In order to defer auto-creation of any routes that already exist, calculate Brick routes only after having loaded all others
|
387
386
|
prepend ::Brick::RouteSet
|
@@ -391,17 +390,6 @@ function changeout(href, param, value) {
|
|
391
390
|
# Just in case it hadn't been done previously when we tried to load the brick initialiser,
|
392
391
|
# go make sure we've loaded additional references (virtual foreign keys).
|
393
392
|
::Brick.load_additional_references
|
394
|
-
|
395
|
-
# Find associative tables that can be set up for has_many :through
|
396
|
-
::Brick.relations.each do |_key, tbl|
|
397
|
-
tbl_cols = tbl[:cols].keys
|
398
|
-
fks = tbl[:fks].each_with_object({}) { |fk, s| s[fk.last[:fk]] = [fk.last[:assoc_name], fk.last[:inverse_table]] if fk.last[:is_bt]; s }
|
399
|
-
# Aside from the primary key and the metadata columns created_at, updated_at, and deleted_at, if this table only has
|
400
|
-
# foreign keys then it can act as an associative table and thus be used with has_many :through.
|
401
|
-
if fks.length > 1 && (tbl_cols - fks.keys - (::Brick.config.metadata_columns || []) - (tbl[:pkey].values.first || [])).length.zero?
|
402
|
-
fks.each { |fk| tbl[:hmt_fks][fk.first] = fk.last }
|
403
|
-
end
|
404
|
-
end
|
405
393
|
end
|
406
394
|
end
|
407
395
|
end
|
@@ -0,0 +1,227 @@
|
|
1
|
+
module Brick
|
2
|
+
# JoinArray and JoinHash
|
3
|
+
#
|
4
|
+
# These JOIN-related collection classes -- JoinArray and its related "partner in crime" JoinHash -- both interact to
|
5
|
+
# more easily build out nested sets of hashes and arrays to be used with ActiveRecord's .joins() method. For example,
|
6
|
+
# if there is an Order, Customer, and Employee model, and Order belongs_to :customer and :employee, then from the
|
7
|
+
# perspective of Order all these three could be JOINed together by referencing the two belongs_to association names:
|
8
|
+
#
|
9
|
+
# Order.joins([:customer, :employee])
|
10
|
+
#
|
11
|
+
# and from the perspective of Employee it would instead use a hash like this, using the has_many :orders association
|
12
|
+
# and the :customer belongs_to:
|
13
|
+
#
|
14
|
+
# Employee.joins({ orders: :customer })
|
15
|
+
#
|
16
|
+
# (in both cases the same three tables are being JOINed, the two approaches differ just based on their starting standpoint.)
|
17
|
+
# These utility classes are designed to make building out any goofy linkages like this pretty simple in a few ways:
|
18
|
+
# ** if the same association is requested more than once then no duplicates.
|
19
|
+
# ** If a bunch of intermediary associations are referenced leading up to a final one then all of them get automatically built
|
20
|
+
# out and added along the way, without any having to previously exist.
|
21
|
+
# ** If one reference was made previously and now another neighbouring one is called for, then what used to be a simple symbol
|
22
|
+
# is automatically graduated into an array so that both members can be held. For instance, if with the Order example above
|
23
|
+
# there was also a LineItem model that belongs_to Order, then let's say you start from LineItem and want to now get all 4
|
24
|
+
# related models. You could start by going through :order to :employee like this:
|
25
|
+
#
|
26
|
+
# line_item_joins = JoinArray.new
|
27
|
+
# line_item_joins[:order] = :employee
|
28
|
+
# => { order: :employee }
|
29
|
+
#
|
30
|
+
# and then add in the reference to :customer like this:
|
31
|
+
#
|
32
|
+
# line_item_joins[:order] = :customer
|
33
|
+
# => { order: [:employee, :customer] }
|
34
|
+
#
|
35
|
+
# and then carry on incrementally building out more JOINs in whatever sequence makes the best sense. This bundle of nested
|
36
|
+
# stuff can then be used to query ActiveRecord like this:
|
37
|
+
#
|
38
|
+
# LineItem.joins(line_item_joins)
|
39
|
+
|
40
|
+
class JoinArray < Array
|
41
|
+
attr_reader :parent, :orig_parent, :parent_key
|
42
|
+
alias _brick_set []=
|
43
|
+
|
44
|
+
def [](*args)
|
45
|
+
if !(key = args[0]).is_a?(Symbol)
|
46
|
+
super
|
47
|
+
else
|
48
|
+
idx = -1
|
49
|
+
# Whenever a JoinHash has a value of a JoinArray with a single member then it is a wrapper, usually for a Symbol
|
50
|
+
matching = find { |x| idx += 1; (x.is_a?(::Brick::JoinArray) && x.first == key) || (x.is_a?(::Brick::JoinHash) && x.key?(key)) || x == key }
|
51
|
+
case matching
|
52
|
+
when ::Brick::JoinHash
|
53
|
+
matching[key]
|
54
|
+
when ::Brick::JoinArray
|
55
|
+
matching.first
|
56
|
+
else
|
57
|
+
::Brick::JoinHash.new.tap do |child|
|
58
|
+
child.instance_variable_set(:@parent, self)
|
59
|
+
child.instance_variable_set(:@parent_key, key) # %%% Use idx instead of key?
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def []=(*args)
|
66
|
+
::Brick::JoinArray.attach_back_to_root(self, args[0], args[1])
|
67
|
+
|
68
|
+
if (key = args[0]).is_a?(Symbol) && ((value = args[1]).is_a?(::Brick::JoinHash) || value.is_a?(Symbol) || value.nil?)
|
69
|
+
# %%% This is for the first symbol added to a JoinArray, cleaning out the leftover {} that is temporarily built out
|
70
|
+
# when doing my_join_array[:value1][:value2] = nil.
|
71
|
+
idx = -1
|
72
|
+
delete_at(idx) if value.nil? && any? { |x| idx += 1; x.is_a?(::Brick::JoinHash) && x.empty? }
|
73
|
+
|
74
|
+
set_matching(key, value)
|
75
|
+
else
|
76
|
+
super
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.attach_back_to_root(collection, key = nil, value = nil)
|
81
|
+
# Create a list of layers which start at the root
|
82
|
+
layers = []
|
83
|
+
layer = collection
|
84
|
+
while layer.parent
|
85
|
+
layers << layer
|
86
|
+
layer = layer.parent
|
87
|
+
end
|
88
|
+
# Go through the layers from root down to child, attaching everything
|
89
|
+
layers.each do |layer|
|
90
|
+
if (prnt = layer.remove_instance_variable(:@parent))
|
91
|
+
layer.instance_variable_set(:@orig_parent, prnt)
|
92
|
+
end
|
93
|
+
case prnt
|
94
|
+
when ::Brick::JoinHash
|
95
|
+
value = if prnt.key?(layer.parent_key)
|
96
|
+
if layer.is_a?(Hash)
|
97
|
+
layer
|
98
|
+
else
|
99
|
+
::Brick::JoinArray.new.replace([prnt.fetch(layer.parent_key, nil), layer])
|
100
|
+
end
|
101
|
+
else
|
102
|
+
layer
|
103
|
+
end
|
104
|
+
# This is as if we did: prnt[layer.parent_key] = value
|
105
|
+
# but calling it that way would attempt to infinitely recurse back onto this overridden version of the []= method,
|
106
|
+
# so we go directly to ._brick_store() instead.
|
107
|
+
prnt._brick_store(layer.parent_key, value)
|
108
|
+
when ::Brick::JoinArray
|
109
|
+
if (key)
|
110
|
+
puts "X1"
|
111
|
+
prnt[layer.parent_key][key] = value
|
112
|
+
else
|
113
|
+
prnt[layer.parent_key] = layer
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def set_matching(key, value)
|
120
|
+
idx = -1
|
121
|
+
matching = find { |x| idx += 1; (x.is_a?(::Brick::JoinArray) && x.first == key) || (x.is_a?(::Brick::JoinHash) && x.key?(key)) || x == key }
|
122
|
+
case matching
|
123
|
+
when ::Brick::JoinHash
|
124
|
+
matching[key] = value
|
125
|
+
when Symbol
|
126
|
+
if value.nil? # If it already exists then no worries
|
127
|
+
matching
|
128
|
+
else
|
129
|
+
# Not yet there, so we will "graduate" this single value into being a key / value pair found in a JoinHash. The
|
130
|
+
# destination hash to be used will be either an existing one if there is a neighbouring JoinHash available, or a
|
131
|
+
# newly-built one placed in the "new_hash" variable if none yet exists.
|
132
|
+
hash = find { |x| x.is_a?(::Brick::JoinHash) } || (new_hash = ::Brick::JoinHash.new)
|
133
|
+
hash._brick_store(key, ::Brick::JoinArray.new.tap { |val_array| val_array.replace([value]) })
|
134
|
+
# hash.instance_variable_set(:@parent, matching.parent) if matching.parent
|
135
|
+
# hash.instance_variable_set(:@parent_key, matching.parent_key) if matching.parent_key
|
136
|
+
|
137
|
+
# When a new JoinHash was created, we place it at the same index where the original lone symbol value was pulled from.
|
138
|
+
# If instead we used an existing JoinHash then since that symbol has now been graduated into a new key / value pair in
|
139
|
+
# the existing JoinHash then we delete the original symbol by its index.
|
140
|
+
new_hash ? _brick_set(idx, new_hash) : delete_at(idx)
|
141
|
+
end
|
142
|
+
when ::Brick::JoinArray # Replace this single thing (usually a Symbol found as a value in a JoinHash)
|
143
|
+
(hash = ::Brick::JoinHash.new)._brick_store(key, value)
|
144
|
+
if matching.parent
|
145
|
+
hash.instance_variable_set(:@parent, matching.parent)
|
146
|
+
hash.instance_variable_set(:@parent_key, matching.parent_key)
|
147
|
+
end
|
148
|
+
_brick_set(idx, hash)
|
149
|
+
else # Doesn't already exist anywhere, so add it to the end of this JoinArray and return the new member
|
150
|
+
if value
|
151
|
+
::Brick::JoinHash.new.tap do |hash|
|
152
|
+
val_collection = if value.is_a?(::Brick::JoinHash)
|
153
|
+
value
|
154
|
+
else
|
155
|
+
::Brick::JoinArray.new.tap { |array| array.replace([value]) }
|
156
|
+
end
|
157
|
+
val_collection.instance_variable_set(:@parent, hash)
|
158
|
+
val_collection.instance_variable_set(:@parent_key, key)
|
159
|
+
hash._brick_store(key, val_collection)
|
160
|
+
hash.instance_variable_set(:@parent, self)
|
161
|
+
hash.instance_variable_set(:@parent_key, length)
|
162
|
+
end
|
163
|
+
else
|
164
|
+
key
|
165
|
+
end.tap { |member| push(member) }
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
class JoinHash < Hash
|
171
|
+
attr_reader :parent, :orig_parent, :parent_key
|
172
|
+
alias _brick_store []=
|
173
|
+
|
174
|
+
def [](*args)
|
175
|
+
if (current = super)
|
176
|
+
current
|
177
|
+
elsif (key = args[0]).is_a?(Symbol)
|
178
|
+
::Brick::JoinHash.new.tap do |child|
|
179
|
+
child.instance_variable_set(:@parent, self)
|
180
|
+
child.instance_variable_set(:@parent_key, key)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def []=(*args)
|
186
|
+
::Brick::JoinArray.attach_back_to_root(self)
|
187
|
+
|
188
|
+
if !(key = args[0]).is_a?(Symbol) || (!(value = args[1]).is_a?(Symbol) && !value.nil?)
|
189
|
+
super # Revert to normal hash behaviour when we're not passed symbols
|
190
|
+
else
|
191
|
+
case (current = fetch(key, nil))
|
192
|
+
when value
|
193
|
+
if value.nil? # Setting a single value where nothing yet exists
|
194
|
+
case orig_parent
|
195
|
+
when ::Brick::JoinHash
|
196
|
+
if self.empty? # Convert this empty hash into a JoinArray
|
197
|
+
orig_parent._brick_store(parent_key, ::Brick::JoinArray.new.replace([key]))
|
198
|
+
else # Call back into []= to use our own logic, this time setting this value from the context of the parent
|
199
|
+
orig_parent[parent_key] = key
|
200
|
+
end
|
201
|
+
when ::Brick::JoinArray
|
202
|
+
orig_parent[parent_key][key] = nil
|
203
|
+
else # No knowledge of any parent, so all we can do is add this single value right here as { key => nil }
|
204
|
+
super
|
205
|
+
end
|
206
|
+
key
|
207
|
+
else # Setting a key / value pair where nothing yet exists
|
208
|
+
puts "X2"
|
209
|
+
super(key, ::Brick::JoinArray.new.replace([value]))
|
210
|
+
value
|
211
|
+
end
|
212
|
+
when Symbol # Upgrade an existing symbol to be a part of our special JoinArray
|
213
|
+
puts "X3"
|
214
|
+
super(key, ::Brick::JoinArray.new.replace([current, value]))
|
215
|
+
when ::Brick::JoinArray # Concatenate new stuff onto any existing JoinArray
|
216
|
+
current.set_matching(value, nil)
|
217
|
+
when ::Brick::JoinHash # Graduate an existing hash into being in an array if things are dissimilar
|
218
|
+
super(key, ::Brick::JoinArray.new.replace([current, value]))
|
219
|
+
value
|
220
|
+
else # Perhaps this is part of some hybrid thing
|
221
|
+
super(key, ::Brick::JoinArray.new.replace([value]))
|
222
|
+
value
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
data/lib/brick/version_number.rb
CHANGED
data/lib/brick.rb
CHANGED
@@ -102,14 +102,37 @@ module Brick
|
|
102
102
|
end
|
103
103
|
|
104
104
|
def get_bts_and_hms(model)
|
105
|
-
model.reflect_on_all_associations.each_with_object([{}, {}]) do |a, s|
|
105
|
+
bts, hms = model.reflect_on_all_associations.each_with_object([{}, {}]) do |a, s|
|
106
|
+
next if !const_defined?(a.name.to_s.singularize.camelize) && ::Brick.config.exclude_tables.include?(a.plural_name)
|
107
|
+
|
108
|
+
# So that we can map an association name to any special alias name used in an AREL query
|
109
|
+
ans = (model._assoc_names[a.name] ||= [])
|
110
|
+
ans << a.klass unless ans.include?(a.klass)
|
106
111
|
case a.macro
|
107
112
|
when :belongs_to
|
108
113
|
s.first[a.foreign_key] = [a.name, a.klass]
|
109
|
-
when :has_many, :has_one
|
114
|
+
when :has_many, :has_one # This gets has_many as well as has_many :through
|
115
|
+
# %%% weed out ones that don't have an available model to reference
|
110
116
|
s.last[a.name] = a
|
111
117
|
end
|
112
118
|
end
|
119
|
+
# Mark has_manys that go to an associative ("join") table so that they are skipped in the UI,
|
120
|
+
# as well as any possible polymorphic associations
|
121
|
+
skip_hms = {}
|
122
|
+
associatives = hms.each_with_object({}) do |hmt, s|
|
123
|
+
if (through = hmt.last.options[:through])
|
124
|
+
skip_hms[through] = nil
|
125
|
+
s[hmt.first] = hms[through] # End up with a hash of HMT names pointing to join-table associations
|
126
|
+
elsif hmt.last.inverse_of.nil?
|
127
|
+
puts "SKIPPING #{hmt.last.name.inspect}"
|
128
|
+
# %%% If we don't do this then below associative.name will find that associative is nil
|
129
|
+
skip_hms[hmt.last.name] = nil
|
130
|
+
end
|
131
|
+
end
|
132
|
+
skip_hms.each do |k, _v|
|
133
|
+
puts hms.delete(k).inspect
|
134
|
+
end
|
135
|
+
[bts, hms, associatives]
|
113
136
|
end
|
114
137
|
|
115
138
|
# Switches Brick auto-models on or off, for all threads
|
@@ -217,6 +240,12 @@ module Brick
|
|
217
240
|
end
|
218
241
|
end
|
219
242
|
|
243
|
+
# Skip showing counts for these specific has_many associations when building auto-generated #index views
|
244
|
+
# @api public
|
245
|
+
def skip_index_hms=(value)
|
246
|
+
Brick.config.skip_index_hms = value
|
247
|
+
end
|
248
|
+
|
220
249
|
# Associations to treat as a has_one
|
221
250
|
# @api public
|
222
251
|
def has_ones=(hos)
|
@@ -254,6 +283,17 @@ module Brick
|
|
254
283
|
ars.each { |fk| ::Brick._add_bt_and_hm(fk[0..2]) }
|
255
284
|
@_additional_references_loaded = true
|
256
285
|
end
|
286
|
+
|
287
|
+
# Find associative tables that can be set up for has_many :through
|
288
|
+
::Brick.relations.each do |_key, tbl|
|
289
|
+
tbl_cols = tbl[:cols].keys
|
290
|
+
fks = tbl[:fks].each_with_object({}) { |fk, s| s[fk.last[:fk]] = [fk.last[:assoc_name], fk.last[:inverse_table]] if fk.last[:is_bt]; s }
|
291
|
+
# Aside from the primary key and the metadata columns created_at, updated_at, and deleted_at, if this table only has
|
292
|
+
# foreign keys then it can act as an associative table and thus be used with has_many :through.
|
293
|
+
if fks.length > 1 && (tbl_cols - fks.keys - (::Brick.config.metadata_columns || []) - (tbl[:pkey].values.first || [])).length.zero?
|
294
|
+
fks.each { |fk| tbl[:hmt_fks][fk.first] = fk.last }
|
295
|
+
end
|
296
|
+
end
|
257
297
|
end
|
258
298
|
|
259
299
|
|
@@ -18,7 +18,7 @@ module Brick
|
|
18
18
|
desc 'Generates an initializer file for configuring Brick'
|
19
19
|
|
20
20
|
def create_initializer_file
|
21
|
-
unless File.
|
21
|
+
unless File.exist?(filename = 'config/initializers/brick.rb')
|
22
22
|
# See if we can make suggestions for additional_references
|
23
23
|
resembles_fks = []
|
24
24
|
possible_additional_references = (relations = ::Brick.relations).each_with_object([]) do |v, s|
|
@@ -47,7 +47,7 @@ module Brick
|
|
47
47
|
if (relations.fetch(f_table = col_down, nil) ||
|
48
48
|
relations.fetch(f_table = ActiveSupport::Inflector.pluralize(col_down), nil)) &&
|
49
49
|
# Looks pretty promising ... just make sure a model file isn't present
|
50
|
-
!File.
|
50
|
+
!File.exist?("app/models/#{ActiveSupport::Inflector.singularize(v.first)}.rb")
|
51
51
|
s << "['#{v.first}', '#{col}', '#{f_table}']"
|
52
52
|
else
|
53
53
|
resembles_fks << "#{v.first}.#{col}"
|
@@ -58,24 +58,24 @@ module Brick
|
|
58
58
|
end
|
59
59
|
|
60
60
|
bar = case possible_additional_references.length
|
61
|
-
|
61
|
+
when 0
|
62
62
|
+"# Brick.additional_references = [['orders', 'customer_id', 'customer'],
|
63
63
|
# ['customer', 'region_id', 'regions']]"
|
64
|
-
|
65
|
-
|
64
|
+
when 1
|
65
|
+
+"# # Here is a possible additional reference that has been auto-identified for the #{ActiveRecord::Base.connection.current_database} database:
|
66
66
|
# Brick.additional_references = [[#{possible_additional_references.first}]"
|
67
|
-
|
68
|
-
|
67
|
+
else
|
68
|
+
+"# # Here are possible additional references that have been auto-identified for the #{ActiveRecord::Base.connection.current_database} database:
|
69
69
|
# Brick.additional_references = [
|
70
70
|
# #{possible_additional_references.join(",\n# ")}
|
71
71
|
# ]"
|
72
|
-
|
72
|
+
end
|
73
73
|
if resembles_fks.length > 0
|
74
74
|
bar << "\n# # Columns named somewhat like a foreign key which you may want to consider:
|
75
75
|
# # #{resembles_fks.join(', ')}"
|
76
76
|
end
|
77
77
|
|
78
|
-
|
78
|
+
create_file(filename, "# frozen_string_literal: true
|
79
79
|
|
80
80
|
# # Settings for the Brick gem
|
81
81
|
# # (By default this auto-creates models, controllers, views, and routes on-the-fly.)
|
@@ -111,11 +111,16 @@ module Brick
|
|
111
111
|
# # to be the primary key.)
|
112
112
|
#{bar}
|
113
113
|
|
114
|
-
# # Skip creating a has_many association for these
|
114
|
+
# # Skip creating a has_many association for these (only retain the belongs_to built from this additional_reference).
|
115
115
|
# # (Uses the same exact three-part format as would define an additional_reference)
|
116
116
|
# # Say for instance that we didn't care to display the favourite colours that users have:
|
117
117
|
# Brick.exclude_hms = [['users', 'favourite_colour_id', 'colours']]
|
118
118
|
|
119
|
+
# # Skip showing counts for these specific has_many associations when building auto-generated #index views.
|
120
|
+
# # When there are related tables with a significant number of records, this can lessen the load on the database
|
121
|
+
# # considerably, sometimes fixing what might appear to be an index page that just \"hangs\" for no apparent reason.
|
122
|
+
Brick.skip_index_hms = ['User.litany_of_woes']
|
123
|
+
|
119
124
|
# # By default primary tables involved in a foreign key relationship will indicate a \"has_many\" relationship pointing
|
120
125
|
# # back to the foreign table. In order to represent a \"has_one\" association instead, an override can be provided
|
121
126
|
# # using the primary model name and the association name which you instead want to have treated as a \"has_one\":
|
@@ -159,7 +164,7 @@ module Brick
|
|
159
164
|
# Brick.default_route_fallback = 'customers' # This defaults to \"customers/index\"
|
160
165
|
# Brick.default_route_fallback = 'orders/outstanding' # Example of a non-RESTful route
|
161
166
|
# Brick.default_route_fallback = '' # Omits setting a default route in the absence of any other
|
162
|
-
"
|
167
|
+
")
|
163
168
|
end
|
164
169
|
end
|
165
170
|
|
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.21
|
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-05-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -221,6 +221,7 @@ files:
|
|
221
221
|
- lib/brick/frameworks/rails/controller.rb
|
222
222
|
- lib/brick/frameworks/rails/engine.rb
|
223
223
|
- lib/brick/frameworks/rspec.rb
|
224
|
+
- lib/brick/join_array.rb
|
224
225
|
- lib/brick/serializers/json.rb
|
225
226
|
- lib/brick/serializers/yaml.rb
|
226
227
|
- lib/brick/util.rb
|