brick 1.0.19 → 1.0.20
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/brick/extensions.rb +275 -45
- data/lib/brick/frameworks/rails/engine.rb +70 -83
- data/lib/brick/join_array.rb +227 -0
- data/lib/brick/version_number.rb +1 -1
- data/lib/brick.rb +25 -2
- data/lib/generators/brick/install_generator.rb +8 -8
- 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: 41b64e2d571540382ce5ecd497f1e1413fde20119d46f1b24e1fae5af428fb45
|
4
|
+
data.tar.gz: 1c26ccf7b8df54fd121546e8daeeb9508561328186af46aea8ba1bd819524532
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e103d061b74402f3fe0b944d06b30c20f053166c706a54f15753afc8cb0c703ae71f83bd88191f985a37180cb9bdafdba918178174df08f58f683a723fcfb73a
|
7
|
+
data.tar.gz: 13a7dce7f4f5789a0d7a0c6cae4adcaf2f44d22d0f59c355d75102a7f08292d80c781f8d33be121801d375fd33fed8bcdf4ef6b56108ef8f4ab4110c1f49e296
|
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
|
59
78
|
end
|
60
|
-
|
79
|
+
dsl
|
80
|
+
end
|
81
|
+
|
82
|
+
# Pass in true or a JoinArray
|
83
|
+
def self.brick_parse_dsl(build_array = nil, prefix = [], translations = {})
|
84
|
+
build_array = ::Brick::JoinArray.new.tap { |ary| ary.replace([build_array]) } if build_array.is_a?(::Brick::JoinHash)
|
85
|
+
build_array = ::Brick::JoinArray.new unless build_array.nil? || build_array.is_a?(Array)
|
86
|
+
members = []
|
87
|
+
bracket_name = nil
|
88
|
+
prefix = [prefix] unless prefix.is_a?(Array)
|
89
|
+
if (dsl = ::Brick.config.model_descrips[name] || brick_get_dsl)
|
90
|
+
klass = nil
|
91
|
+
dsl.each_char do |ch|
|
92
|
+
if bracket_name
|
93
|
+
if ch == ']' # Time to process a bracketed thing?
|
94
|
+
parts = bracket_name.split('.')
|
95
|
+
first_parts = parts[0..-2].map { |part| klass = klass.reflect_on_association(part_sym = part.to_sym).klass; part_sym }
|
96
|
+
parts = prefix + first_parts + [parts[-1]]
|
97
|
+
if parts.length > 1
|
98
|
+
s = build_array
|
99
|
+
parts[0..-3].each { |v| s = s[v.to_sym] }
|
100
|
+
s[parts[-2]] = nil # unless parts[-2].empty? # Using []= will "hydrate" any missing part(s) in our whole series
|
101
|
+
translations[parts[0..-2].join('.')] = klass
|
102
|
+
end
|
103
|
+
members << parts
|
104
|
+
bracket_name = nil
|
105
|
+
else
|
106
|
+
bracket_name << ch
|
107
|
+
end
|
108
|
+
elsif ch == '['
|
109
|
+
bracket_name = +''
|
110
|
+
klass = self
|
111
|
+
end
|
112
|
+
end
|
113
|
+
else # With no DSL available, still put this prefix into the JoinArray so we can get primary key (ID) info from this table
|
114
|
+
x = prefix.each_with_object(build_array) { |v, s| s[v.to_sym] }
|
115
|
+
x[prefix[-1]] = nil unless prefix.empty? # Using []= will "hydrate" any missing part(s) in our whole series
|
116
|
+
end
|
117
|
+
members
|
118
|
+
end
|
119
|
+
|
120
|
+
# If available, parse simple DSL attached to a model in order to provide a friendlier name.
|
121
|
+
# Object property names can be referenced in square brackets like this:
|
122
|
+
# { 'User' => '[profile.firstname] [profile.lastname]' }
|
123
|
+
def brick_descrip
|
124
|
+
self.class.brick_descrip(self)
|
125
|
+
end
|
126
|
+
|
127
|
+
def self.brick_descrip(obj, data = nil, pk_alias = nil)
|
128
|
+
if (dsl = ::Brick.config.model_descrips[(klass = self).name] || klass.brick_get_dsl)
|
129
|
+
idx = -1
|
61
130
|
caches = {}
|
62
131
|
output = +''
|
63
132
|
is_brackets_have_content = false
|
@@ -65,18 +134,23 @@ module ActiveRecord
|
|
65
134
|
dsl.each_char do |ch|
|
66
135
|
if bracket_name
|
67
136
|
if ch == ']' # Time to process a bracketed thing?
|
68
|
-
|
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,23 +276,86 @@ module ActiveRecord
|
|
116
276
|
when 2
|
117
277
|
assoc_name = ks.first.to_sym
|
118
278
|
# Make sure it's a good association name and that the model has that column name
|
119
|
-
next unless klass.reflect_on_association(assoc_name)&.klass&.columns&.map(&:name)&.include?(ks.last)
|
120
|
-
|
121
|
-
|
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
|
297
|
+
end
|
298
|
+
|
299
|
+
# Search for BT, HM, and HMT DSL stuff
|
300
|
+
translations = {}
|
301
|
+
if is_add_bts || is_add_hms
|
302
|
+
bts, hms, associatives = ::Brick.get_bts_and_hms(klass)
|
303
|
+
bts.each do |_k, bt|
|
304
|
+
# join_array[bt.first] = nil # Store this relation name in our special collection for .joins()
|
305
|
+
bt_descrip[bt.first] = [bt.last, bt.last.brick_parse_dsl(join_array, bt.first, translations)]
|
306
|
+
end
|
307
|
+
hms.each do |k, hm|
|
308
|
+
join_array[k] = nil # Store this relation name in our special collection for .joins()
|
309
|
+
hm_counts[k] = nil # Placeholder that will be filled in once we know the proper table alias
|
310
|
+
end
|
129
311
|
end
|
312
|
+
where!(wheres) unless wheres.empty?
|
313
|
+
if join_array.present?
|
314
|
+
left_outer_joins!(join_array) # joins!(join_array)
|
315
|
+
# Without working from a duplicate, touching the AREL ast tree sets the @arel instance variable, which causes the relation to be immutable.
|
316
|
+
(rel_dupe = dup)._arel_alias_names
|
317
|
+
core_selects = selects.dup
|
318
|
+
groups = []
|
319
|
+
chains = rel_dupe._brick_chains
|
320
|
+
id_for_tables = {}
|
321
|
+
bt_columns = bt_descrip.each_with_object([]) do |v, s|
|
322
|
+
tbl_name = chains[v.last.first].first
|
323
|
+
if (id_col = v.last.first.primary_key) && !id_for_tables.key?(tbl_name)
|
324
|
+
groups << (unaliased = "#{tbl_name}.#{id_col}")
|
325
|
+
selects << "#{unaliased} AS \"#{(id_alias = id_for_tables[tbl_name] = "_brfk_#{v.first}__#{id_col}")}\""
|
326
|
+
v.last << id_alias
|
327
|
+
end
|
328
|
+
if (col_name = v.last[1].last&.last)
|
329
|
+
v.last[1].map { |x| [translations[x[0..-2].map(&:to_s).join('.')], x.last] }.each_with_index do |sel_col, idx|
|
330
|
+
groups << (unaliased = "#{tbl_name = chains[sel_col.first].first}.#{sel_col.last}")
|
331
|
+
# col_name is weak when there are multiple, using sel_col.last instead
|
332
|
+
tbl_name2 = tbl_name.start_with?('public.') ? tbl_name[7..-1] : tbl_name
|
333
|
+
selects << "#{unaliased} AS \"#{(col_alias = "_brfk_#{tbl_name2}__#{sel_col.last}")}\""
|
334
|
+
v.last[1][idx] << col_alias
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
338
|
+
group!(core_selects + groups) if hm_counts.any? # + bt_columns
|
339
|
+
join_array.each do |assoc_name|
|
340
|
+
# %%% Need to support {user: :profile}
|
341
|
+
next unless assoc_name.is_a?(Symbol)
|
342
|
+
|
343
|
+
klass = reflect_on_association(assoc_name)&.klass
|
344
|
+
table_alias = chains[klass].length > 1 ? chains[klass].shift : chains[klass].first
|
345
|
+
_assoc_names[assoc_name] = [table_alias, klass]
|
346
|
+
end
|
347
|
+
# Copy entries over
|
348
|
+
hm_counts.keys.each do |k|
|
349
|
+
hm_counts[k] = _assoc_names[k]
|
350
|
+
end
|
351
|
+
end
|
352
|
+
wheres unless wheres.empty? # Return the specific parameters that we did use
|
130
353
|
end
|
131
354
|
end
|
132
355
|
|
133
356
|
module Inheritance
|
134
357
|
module ClassMethods
|
135
|
-
|
358
|
+
private
|
136
359
|
|
137
360
|
alias _brick_find_sti_class find_sti_class
|
138
361
|
def find_sti_class(type_name)
|
@@ -145,10 +368,8 @@ module ActiveRecord
|
|
145
368
|
module_prefixes = type_name.split('::')
|
146
369
|
module_prefixes.unshift('') unless module_prefixes.first.blank?
|
147
370
|
module_name = module_prefixes[0..-2].join('::')
|
148
|
-
if ::Brick.config.sti_namespace_prefixes&.key?("::#{module_name}::") ||
|
149
|
-
|
150
|
-
_brick_find_sti_class(type_name)
|
151
|
-
elsif File.exist?(candidate_file = Rails.root.join('app/models' + module_prefixes.map(&:underscore).join('/') + '.rb'))
|
371
|
+
if (snp = ::Brick.config.sti_namespace_prefixes)&.key?("::#{module_name}::") || snp&.key?("#{module_name}::") ||
|
372
|
+
File.exist?(candidate_file = Rails.root.join('app/models' + module_prefixes.map(&:underscore).join('/') + '.rb'))
|
152
373
|
_brick_find_sti_class(type_name) # Find this STI class normally
|
153
374
|
else
|
154
375
|
# Build missing prefix modules if they don't yet exist
|
@@ -227,10 +448,9 @@ class Object
|
|
227
448
|
end
|
228
449
|
|
229
450
|
relations = ::Brick.instance_variable_get(:@relations)[ActiveRecord::Base.connection_pool.object_id] || {}
|
230
|
-
|
231
|
-
result = if is_controllers_enabled && class_name.end_with?('Controller') && (plural_class_name = class_name[0..-11]).length.positive?
|
451
|
+
result = if ::Brick.enable_controllers? && class_name.end_with?('Controller') && (plural_class_name = class_name[0..-11]).length.positive?
|
232
452
|
# Otherwise now it's up to us to fill in the gaps
|
233
|
-
if (model =
|
453
|
+
if (model = plural_class_name.singularize.constantize)
|
234
454
|
# if it's a controller and no match or a model doesn't really use the same table name, eager load all models and try to find a model class of the right name.
|
235
455
|
build_controller(class_name, plural_class_name, model, relations)
|
236
456
|
end
|
@@ -281,6 +501,7 @@ class Object
|
|
281
501
|
end
|
282
502
|
return
|
283
503
|
end
|
504
|
+
|
284
505
|
if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, nil))
|
285
506
|
is_sti = true
|
286
507
|
else
|
@@ -442,13 +663,22 @@ class Object
|
|
442
663
|
|
443
664
|
code << " def index\n"
|
444
665
|
code << " @#{table_name} = #{model.name}#{model.primary_key ? ".order(#{model.primary_key.inspect})" : '.all'}\n"
|
445
|
-
code << " @#{table_name}.
|
666
|
+
code << " @#{table_name}.brick_select(params)\n"
|
446
667
|
code << " end\n"
|
447
668
|
self.define_method :index do
|
448
669
|
::Brick.set_db_schema(params)
|
449
|
-
ar_relation = model.primary_key ? model.order(model.primary_key) : model.all
|
450
|
-
|
451
|
-
|
670
|
+
ar_relation = model.all # model.primary_key ? model.order(model.primary_key) : model.all
|
671
|
+
@_brick_params = ar_relation.brick_select(params, (selects = []), (bt_descrip = {}), (hm_counts = {}), (join_array = ::Brick::JoinArray.new))
|
672
|
+
# %%% Add custom HM count columns
|
673
|
+
# %%% What happens when the PK is composite?
|
674
|
+
counts = hm_counts.each_with_object([]) { |v, s| s << "COUNT(DISTINCT #{v.last.first}.#{v.last.last.primary_key}) AS _br_#{v.first}_ct" }
|
675
|
+
puts counts.inspect
|
676
|
+
# *selects,
|
677
|
+
instance_variable_set("@#{table_name}".to_sym, ar_relation.dup._select!(*selects, *counts))
|
678
|
+
# binding.pry
|
679
|
+
@_brick_bt_descrip = bt_descrip
|
680
|
+
@_brick_hm_counts = hm_counts
|
681
|
+
@_brick_join_array = join_array
|
452
682
|
end
|
453
683
|
|
454
684
|
if model.primary_key
|
@@ -7,10 +7,12 @@ module Brick
|
|
7
7
|
# paths['app/models'] << 'lib/brick/frameworks/active_record/models'
|
8
8
|
config.brick = ActiveSupport::OrderedOptions.new
|
9
9
|
ActiveSupport.on_load(:before_initialize) do |app|
|
10
|
+
is_development = (ENV['RAILS_ENV'] || ENV['RACK_ENV']) == 'development'
|
10
11
|
::Brick.enable_models = app.config.brick.fetch(:enable_models, true)
|
11
|
-
::Brick.enable_controllers = app.config.brick.fetch(:enable_controllers,
|
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)
|
@@ -52,10 +54,11 @@ module Brick
|
|
52
54
|
# args will be something like: ["index", ["categories"]]
|
53
55
|
model = args[1].map(&:camelize).join('::').singularize.constantize
|
54
56
|
if is_template_exists = model && (
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
57
|
+
['index', 'show'].include?(args.first) || # Everything has index and show
|
58
|
+
# Only CUD stuff has create / update / destroy
|
59
|
+
(!model.is_view? && ['new', 'create', 'edit', 'update', 'destroy'].include?(args.first))
|
60
|
+
)
|
61
|
+
@_brick_model = model
|
59
62
|
end
|
60
63
|
end
|
61
64
|
is_template_exists
|
@@ -63,59 +66,43 @@ module Brick
|
|
63
66
|
|
64
67
|
alias :_brick_find_template :find_template
|
65
68
|
def find_template(*args, **options)
|
66
|
-
|
67
|
-
model_name = @_brick_model.name
|
68
|
-
pk = @_brick_model.primary_key
|
69
|
-
obj_name = model_name.underscore
|
70
|
-
table_name = model_name.pluralize.underscore
|
71
|
-
# This gets has_many as well as has_many :through
|
72
|
-
# %%% weed out ones that don't have an available model to reference
|
73
|
-
bts, hms = ::Brick.get_bts_and_hms(@_brick_model)
|
74
|
-
# Mark has_manys that go to an associative ("join") table so that they are skipped in the UI,
|
75
|
-
# as well as any possible polymorphic associations
|
76
|
-
exclude_hms = {}
|
77
|
-
associatives = hms.each_with_object({}) do |hmt, s|
|
78
|
-
if (through = hmt.last.options[:through])
|
79
|
-
exclude_hms[through] = nil
|
80
|
-
s[hmt.first] = hms[through] # End up with a hash of HMT names pointing to join-table associations
|
81
|
-
elsif hmt.last.inverse_of.nil?
|
82
|
-
puts "SKIPPING #{hmt.last.name.inspect}"
|
83
|
-
# %%% If we don't do this then below associative.name will find that associative is nil
|
84
|
-
exclude_hms[hmt.last.name] = nil
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
hms_columns = +'' # Used for 'index'
|
89
|
-
hms_headers = hms.each_with_object([]) do |hm, s|
|
90
|
-
next if exclude_hms.key?((hm_assoc = hm.last).name)
|
69
|
+
return _brick_find_template(*args, **options) unless @_brick_model
|
91
70
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
71
|
+
model_name = @_brick_model.name
|
72
|
+
pk = @_brick_model.primary_key
|
73
|
+
obj_name = model_name.underscore
|
74
|
+
table_name = model_name.pluralize.underscore
|
75
|
+
bts, hms, associatives = ::Brick.get_bts_and_hms(@_brick_model) # This gets BT and HM and also has_many :through (HMT)
|
76
|
+
hms_columns = +'' # Used for 'index'
|
77
|
+
hms_headers = hms.each_with_object([]) do |hm, s|
|
78
|
+
hm_assoc = hm.last
|
79
|
+
if args.first == 'index'
|
80
|
+
hm_fk_name = if hm_assoc.options[:through]
|
81
|
+
associative = associatives[hm_assoc.name]
|
82
|
+
"'#{associative.name}.#{associative.foreign_key}'"
|
83
|
+
else
|
84
|
+
hm_assoc.foreign_key
|
85
|
+
end
|
86
|
+
hms_columns << if hm_assoc.macro == :has_many
|
100
87
|
"<td>
|
101
|
-
<%=
|
88
|
+
<%= ct = #{obj_name}._br_#{hm.first}_ct
|
89
|
+
link_to \"#\{ct\} #{hm.first}\", #{hm_assoc.klass.name.underscore.pluralize}_path({ #{hm_fk_name}: #{obj_name}.#{pk} }) unless ct.zero? %>
|
102
90
|
</td>\n"
|
103
|
-
|
91
|
+
else # has_one
|
104
92
|
"<td>
|
105
93
|
<%= obj = #{obj_name}.#{hm.first}; link_to(obj.brick_descrip, obj) if obj %>
|
106
94
|
</td>\n"
|
107
|
-
|
108
|
-
end
|
109
|
-
s << [hm_assoc, "H#{hm_assoc.macro == :has_one ? 'O' : 'M'}#{'T' if hm_assoc.options[:through]} #{hm.first}"]
|
95
|
+
end
|
110
96
|
end
|
97
|
+
s << [hm_assoc, "H#{hm_assoc.macro == :has_one ? 'O' : 'M'}#{'T' if hm_assoc.options[:through]} #{hm.first}"]
|
111
98
|
end
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
99
|
+
|
100
|
+
schema_options = ::Brick.db_schemas.each_with_object(+'') { |v, s| s << "<option value=\"#{v}\">#{v}</option>" }.html_safe
|
101
|
+
# %%% If we are not auto-creating controllers (or routes) then omit by default, and if enabled anyway, such as in a development
|
102
|
+
# environment or whatever, then get either the controllers or routes list instead
|
103
|
+
table_options = (::Brick.relations.keys - ::Brick.config.exclude_tables)
|
104
|
+
.each_with_object(+'') { |v, s| s << "<option value=\"#{v.underscore.pluralize}\">#{v}</option>" }.html_safe
|
105
|
+
css = +"<style>
|
119
106
|
table {
|
120
107
|
border-collapse: collapse;
|
121
108
|
margin: 25px 0;
|
@@ -189,7 +176,12 @@ def hide_bcrypt(val)
|
|
189
176
|
is_bcrypt?(val) ? '(hidden)' : val
|
190
177
|
end %>"
|
191
178
|
|
192
|
-
|
179
|
+
if ['index', 'show', 'update'].include?(args.first)
|
180
|
+
# Example: <% bts = { "site_id" => [:site, Site, "id"], "study_id" => [:study, Study, "id"], "study_country_id" => [:study_country, StudyCountry, "id"], "user_id" => [:user, User, "id"], "role_id" => [:role, Role, "id"] } %>
|
181
|
+
css << "<% bts = { #{bts.each_with_object([]) { |v, s| s << "#{v.first.inspect} => [#{v.last.first.inspect}, #{v.last[1].name}, #{v.last[1].primary_key.inspect}]"}.join(', ')} } %>"
|
182
|
+
end
|
183
|
+
|
184
|
+
script = "<script>
|
193
185
|
var schemaSelect = document.getElementById(\"schema\");
|
194
186
|
var brickSchema;
|
195
187
|
if (schemaSelect) {
|
@@ -243,9 +235,8 @@ function changeout(href, param, value) {
|
|
243
235
|
return hrefParts[0] + \"?\" + Object.keys(params).reduce(function (s, v) { s.push(v + \"=\" + params[v]); return s; }, []).join(\"&\");
|
244
236
|
}
|
245
237
|
</script>"
|
246
|
-
|
247
|
-
|
248
|
-
when 'index'
|
238
|
+
inline = case args.first
|
239
|
+
when 'index'
|
249
240
|
"#{css}
|
250
241
|
<p style=\"color: green\"><%= notice %></p>#{"
|
251
242
|
<select id=\"schema\">#{schema_options}</select>" if ::Brick.db_schemas.length > 1}
|
@@ -253,9 +244,8 @@ function changeout(href, param, value) {
|
|
253
244
|
<h1>#{model_name.pluralize}</h1>
|
254
245
|
<% if @_brick_params&.present? %><h3>where <%= @_brick_params.each_with_object([]) { |v, s| s << \"#\{v.first\} = #\{v.last.inspect\}\" }.join(', ') %></h3><% end %>
|
255
246
|
<table id=\"#{table_name}\">
|
256
|
-
<thead><tr>#{
|
257
|
-
<%
|
258
|
-
@#{table_name}.columns.map(&:name).each do |col| %>
|
247
|
+
<thead><tr>#{'<th></th>' if pk}
|
248
|
+
<% @#{table_name}.columns.map(&:name).each do |col| %>
|
259
249
|
<% next if col == '#{pk}' || ::Brick.config.metadata_columns.include?(col) %>
|
260
250
|
<th>
|
261
251
|
<% if (bt = bts[col]) %>
|
@@ -273,14 +263,14 @@ function changeout(href, param, value) {
|
|
273
263
|
<tr>#{"
|
274
264
|
<td><%= link_to '⇛', #{obj_name}_path(#{obj_name}.#{pk}), { class: 'big-arrow' } %></td>" if pk}
|
275
265
|
<% #{obj_name}.attributes.each do |k, val| %>
|
276
|
-
<% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) %>
|
266
|
+
<% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) || k.start_with?('_brfk_') || (k.start_with?('_br_') && k.end_with?('_ct')) %>
|
277
267
|
<td>
|
278
268
|
<% if (bt = bts[k]) %>
|
279
|
-
<%#
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
269
|
+
<%# binding.pry if bt.first == :user %>
|
270
|
+
<% bt_txt = bt[1].brick_descrip(#{obj_name}, @_brick_bt_descrip[bt.first][1].map { |z| #{obj_name}.send(z.last) }, @_brick_bt_descrip[bt.first][2]) %>
|
271
|
+
<% bt_id_col = @_brick_bt_descrip[bt.first][2]; bt_id = #{obj_name}.send(bt_id_col) if bt_id_col %>
|
272
|
+
<%= bt_id ? link_to(bt_txt, send(\"#\{bt_obj_path_base = bt[1].name.underscore\}_path\".to_sym, bt_id)) : bt_txt %>
|
273
|
+
<%#= Previously was: bt_obj = bt[1].find_by(bt[2] => val); link_to(bt_obj.brick_descrip, send(\"#\{bt_obj_path_base = bt[1].name.underscore\}_path\".to_sym, bt_obj.send(bt[1].primary_key.to_sym))) if bt_obj %>
|
284
274
|
<% else %>
|
285
275
|
<%= hide_bcrypt(val) %>
|
286
276
|
<% end %>
|
@@ -295,7 +285,7 @@ function changeout(href, param, value) {
|
|
295
285
|
|
296
286
|
#{"<hr><%= link_to \"New #{obj_name}\", new_#{obj_name}_path %>" unless @_brick_model.is_view?}
|
297
287
|
#{script}"
|
298
|
-
|
288
|
+
when 'show', 'update'
|
299
289
|
"#{css}
|
300
290
|
<p style=\"color: green\"><%= notice %></p>#{"
|
301
291
|
<select id=\"schema\">#{schema_options}</select>" if ::Brick.db_schemas.length > 1}
|
@@ -308,8 +298,7 @@ function changeout(href, param, value) {
|
|
308
298
|
# url = send(:#{model_name.underscore}_path, obj.#{pk})
|
309
299
|
form_for(obj) do |f| %>
|
310
300
|
<table>
|
311
|
-
<%
|
312
|
-
@#{obj_name}.first.attributes.each do |k, val| %>
|
301
|
+
<% @#{obj_name}.first.attributes.each do |k, val| %>
|
313
302
|
<tr>
|
314
303
|
<% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) %>
|
315
304
|
<th class=\"show-field\">
|
@@ -331,7 +320,7 @@ function changeout(href, param, value) {
|
|
331
320
|
html_options = { prompt: \"Select #\{bt_name\}\" }
|
332
321
|
html_options[:class] = 'dimmed' unless val %>
|
333
322
|
<%= f.select k.to_sym, bt[3], { value: val || '^^^brick_NULL^^^' }, html_options %>
|
334
|
-
<%= bt_obj = bt[1].find_by(bt[2] => val); link_to('⇛', send(\"#\{
|
323
|
+
<%= bt_obj = bt[1].find_by(bt[2] => val); link_to('⇛', send(\"#\{bt_obj_path_base = bt_name.underscore\}_path\".to_sym, bt_obj.send(bt[1].primary_key.to_sym)), { class: 'show-arrow' }) if bt_obj %>
|
335
324
|
<% else case #{model_name}.column_for_attribute(k).type
|
336
325
|
when :string, :text %>
|
337
326
|
<% if is_bcrypt?(val) # || .readonly? %>
|
@@ -342,10 +331,10 @@ function changeout(href, param, value) {
|
|
342
331
|
<% when :boolean %>
|
343
332
|
<%= f.check_box k.to_sym %>
|
344
333
|
<% when :integer, :decimal, :float, :date, :datetime, :time, :timestamp
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
334
|
+
# What happens when keys are UUID?
|
335
|
+
# Postgres naturally uses the +uuid_generate_v4()+ function from the uuid-ossp extension
|
336
|
+
# If it's not yet enabled then: enable_extension 'uuid-ossp'
|
337
|
+
# ActiveUUID gem created a new :uuid type %>
|
349
338
|
<%= val %>
|
350
339
|
<% when :binary, :primary_key %>
|
351
340
|
<% end %>
|
@@ -359,6 +348,7 @@ function changeout(href, param, value) {
|
|
359
348
|
|
360
349
|
#{hms_headers.map do |hm|
|
361
350
|
next unless (pk = hm.first.klass.primary_key)
|
351
|
+
|
362
352
|
"<table id=\"#{hm_name = hm.first.name.to_s}\">
|
363
353
|
<tr><th>#{hm.last}</th></tr>
|
364
354
|
<% collection = @#{obj_name}.first.#{hm_name}
|
@@ -374,19 +364,16 @@ function changeout(href, param, value) {
|
|
374
364
|
<% end %>
|
375
365
|
#{script}"
|
376
366
|
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
else
|
383
|
-
_brick_find_template(*args, **options)
|
384
|
-
end
|
367
|
+
end
|
368
|
+
# As if it were an inline template (see #determine_template in actionview-5.2.6.2/lib/action_view/renderer/template_renderer.rb)
|
369
|
+
keys = options.has_key?(:locals) ? options[:locals].keys : []
|
370
|
+
handler = ActionView::Template.handler_for_extension(options[:type] || 'erb')
|
371
|
+
ActionView::Template.new(inline, "auto-generated #{args.first} template", handler, locals: keys)
|
385
372
|
end
|
386
373
|
end
|
387
374
|
end
|
388
375
|
|
389
|
-
if ::Brick.enable_routes?
|
376
|
+
if ::Brick.enable_routes?
|
390
377
|
ActionDispatch::Routing::RouteSet.class_exec do
|
391
378
|
# In order to defer auto-creation of any routes that already exist, calculate Brick routes only after having loaded all others
|
392
379
|
prepend ::Brick::RouteSet
|
@@ -0,0 +1,227 @@
|
|
1
|
+
module Brick
|
2
|
+
# JoinArray and JoinHash
|
3
|
+
#
|
4
|
+
# These JOIN-related collection classes -- JoinArray and its related "partner in crime" JoinHash -- both interact to
|
5
|
+
# more easily build out nested sets of hashes and arrays to be used with ActiveRecord's .joins() method. For example,
|
6
|
+
# if there is an Order, Customer, and Employee model, and Order belongs_to :customer and :employee, then from the
|
7
|
+
# perspective of Order all these three could be JOINed together by referencing the two belongs_to association names:
|
8
|
+
#
|
9
|
+
# Order.joins([:customer, :employee])
|
10
|
+
#
|
11
|
+
# and from the perspective of Employee it would instead use a hash like this, using the has_many :orders association
|
12
|
+
# and the :customer belongs_to:
|
13
|
+
#
|
14
|
+
# Employee.joins({ orders: :customer })
|
15
|
+
#
|
16
|
+
# (in both cases the same three tables are being JOINed, the two approaches differ just based on their starting standpoint.)
|
17
|
+
# These utility classes are designed to make building out any goofy linkages like this pretty simple in a few ways:
|
18
|
+
# ** if the same association is requested more than once then no duplicates.
|
19
|
+
# ** If a bunch of intermediary associations are referenced leading up to a final one then all of them get automatically built
|
20
|
+
# out and added along the way, without any having to previously exist.
|
21
|
+
# ** If one reference was made previously and now another neighbouring one is called for, then what used to be a simple symbol
|
22
|
+
# is automatically graduated into an array so that both members can be held. For instance, if with the Order example above
|
23
|
+
# there was also a LineItem model that belongs_to Order, then let's say you start from LineItem and want to now get all 4
|
24
|
+
# related models. You could start by going through :order to :employee like this:
|
25
|
+
#
|
26
|
+
# line_item_joins = JoinArray.new
|
27
|
+
# line_item_joins[:order] = :employee
|
28
|
+
# => { order: :employee }
|
29
|
+
#
|
30
|
+
# and then add in the reference to :customer like this:
|
31
|
+
#
|
32
|
+
# line_item_joins[:order] = :customer
|
33
|
+
# => { order: [:employee, :customer] }
|
34
|
+
#
|
35
|
+
# and then carry on incrementally building out more JOINs in whatever sequence makes the best sense. This bundle of nested
|
36
|
+
# stuff can then be used to query ActiveRecord like this:
|
37
|
+
#
|
38
|
+
# LineItem.joins(line_item_joins)
|
39
|
+
|
40
|
+
class JoinArray < Array
|
41
|
+
attr_reader :parent, :orig_parent, :parent_key
|
42
|
+
alias _brick_set []=
|
43
|
+
|
44
|
+
def [](*args)
|
45
|
+
if !(key = args[0]).is_a?(Symbol)
|
46
|
+
super
|
47
|
+
else
|
48
|
+
idx = -1
|
49
|
+
# Whenever a JoinHash has a value of a JoinArray with a single member then it is a wrapper, usually for a Symbol
|
50
|
+
matching = find { |x| idx += 1; (x.is_a?(::Brick::JoinArray) && x.first == key) || (x.is_a?(::Brick::JoinHash) && x.key?(key)) || x == key }
|
51
|
+
case matching
|
52
|
+
when ::Brick::JoinHash
|
53
|
+
matching[key]
|
54
|
+
when ::Brick::JoinArray
|
55
|
+
matching.first
|
56
|
+
else
|
57
|
+
::Brick::JoinHash.new.tap do |child|
|
58
|
+
child.instance_variable_set(:@parent, self)
|
59
|
+
child.instance_variable_set(:@parent_key, key) # %%% Use idx instead of key?
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def []=(*args)
|
66
|
+
::Brick::JoinArray.attach_back_to_root(self, args[0], args[1])
|
67
|
+
|
68
|
+
if (key = args[0]).is_a?(Symbol) && ((value = args[1]).is_a?(::Brick::JoinHash) || value.is_a?(Symbol) || value.nil?)
|
69
|
+
# %%% This is for the first symbol added to a JoinArray, cleaning out the leftover {} that is temporarily built out
|
70
|
+
# when doing my_join_array[:value1][:value2] = nil.
|
71
|
+
idx = -1
|
72
|
+
delete_at(idx) if value.nil? && any? { |x| idx += 1; x.is_a?(::Brick::JoinHash) && x.empty? }
|
73
|
+
|
74
|
+
set_matching(key, value)
|
75
|
+
else
|
76
|
+
super
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.attach_back_to_root(collection, key = nil, value = nil)
|
81
|
+
# Create a list of layers which start at the root
|
82
|
+
layers = []
|
83
|
+
layer = collection
|
84
|
+
while layer.parent
|
85
|
+
layers << layer
|
86
|
+
layer = layer.parent
|
87
|
+
end
|
88
|
+
# Go through the layers from root down to child, attaching everything
|
89
|
+
layers.each do |layer|
|
90
|
+
if (prnt = layer.remove_instance_variable(:@parent))
|
91
|
+
layer.instance_variable_set(:@orig_parent, prnt)
|
92
|
+
end
|
93
|
+
case prnt
|
94
|
+
when ::Brick::JoinHash
|
95
|
+
value = if prnt.key?(layer.parent_key)
|
96
|
+
if layer.is_a?(Hash)
|
97
|
+
layer
|
98
|
+
else
|
99
|
+
::Brick::JoinArray.new.replace([prnt.fetch(layer.parent_key, nil), layer])
|
100
|
+
end
|
101
|
+
else
|
102
|
+
layer
|
103
|
+
end
|
104
|
+
# This is as if we did: prnt[layer.parent_key] = value
|
105
|
+
# but calling it that way would attempt to infinitely recurse back onto this overridden version of the []= method,
|
106
|
+
# so we go directly to ._brick_store() instead.
|
107
|
+
prnt._brick_store(layer.parent_key, value)
|
108
|
+
when ::Brick::JoinArray
|
109
|
+
if (key)
|
110
|
+
puts "X1"
|
111
|
+
prnt[layer.parent_key][key] = value
|
112
|
+
else
|
113
|
+
prnt[layer.parent_key] = layer
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def set_matching(key, value)
|
120
|
+
idx = -1
|
121
|
+
matching = find { |x| idx += 1; (x.is_a?(::Brick::JoinArray) && x.first == key) || (x.is_a?(::Brick::JoinHash) && x.key?(key)) || x == key }
|
122
|
+
case matching
|
123
|
+
when ::Brick::JoinHash
|
124
|
+
matching[key] = value
|
125
|
+
when Symbol
|
126
|
+
if value.nil? # If it already exists then no worries
|
127
|
+
matching
|
128
|
+
else
|
129
|
+
# Not yet there, so we will "graduate" this single value into being a key / value pair found in a JoinHash. The
|
130
|
+
# destination hash to be used will be either an existing one if there is a neighbouring JoinHash available, or a
|
131
|
+
# newly-built one placed in the "new_hash" variable if none yet exists.
|
132
|
+
hash = find { |x| x.is_a?(::Brick::JoinHash) } || (new_hash = ::Brick::JoinHash.new)
|
133
|
+
hash._brick_store(key, ::Brick::JoinArray.new.tap { |val_array| val_array.replace([value]) })
|
134
|
+
# hash.instance_variable_set(:@parent, matching.parent) if matching.parent
|
135
|
+
# hash.instance_variable_set(:@parent_key, matching.parent_key) if matching.parent_key
|
136
|
+
|
137
|
+
# When a new JoinHash was created, we place it at the same index where the original lone symbol value was pulled from.
|
138
|
+
# If instead we used an existing JoinHash then since that symbol has now been graduated into a new key / value pair in
|
139
|
+
# the existing JoinHash then we delete the original symbol by its index.
|
140
|
+
new_hash ? _brick_set(idx, new_hash) : delete_at(idx)
|
141
|
+
end
|
142
|
+
when ::Brick::JoinArray # Replace this single thing (usually a Symbol found as a value in a JoinHash)
|
143
|
+
(hash = ::Brick::JoinHash.new)._brick_store(key, value)
|
144
|
+
if matching.parent
|
145
|
+
hash.instance_variable_set(:@parent, matching.parent)
|
146
|
+
hash.instance_variable_set(:@parent_key, matching.parent_key)
|
147
|
+
end
|
148
|
+
_brick_set(idx, hash)
|
149
|
+
else # Doesn't already exist anywhere, so add it to the end of this JoinArray and return the new member
|
150
|
+
if value
|
151
|
+
::Brick::JoinHash.new.tap do |hash|
|
152
|
+
val_collection = if value.is_a?(::Brick::JoinHash)
|
153
|
+
value
|
154
|
+
else
|
155
|
+
::Brick::JoinArray.new.tap { |array| array.replace([value]) }
|
156
|
+
end
|
157
|
+
val_collection.instance_variable_set(:@parent, hash)
|
158
|
+
val_collection.instance_variable_set(:@parent_key, key)
|
159
|
+
hash._brick_store(key, val_collection)
|
160
|
+
hash.instance_variable_set(:@parent, self)
|
161
|
+
hash.instance_variable_set(:@parent_key, length)
|
162
|
+
end
|
163
|
+
else
|
164
|
+
key
|
165
|
+
end.tap { |member| push(member) }
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
class JoinHash < Hash
|
171
|
+
attr_reader :parent, :orig_parent, :parent_key
|
172
|
+
alias _brick_store []=
|
173
|
+
|
174
|
+
def [](*args)
|
175
|
+
if (current = super)
|
176
|
+
current
|
177
|
+
elsif (key = args[0]).is_a?(Symbol)
|
178
|
+
::Brick::JoinHash.new.tap do |child|
|
179
|
+
child.instance_variable_set(:@parent, self)
|
180
|
+
child.instance_variable_set(:@parent_key, key)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def []=(*args)
|
186
|
+
::Brick::JoinArray.attach_back_to_root(self)
|
187
|
+
|
188
|
+
if !(key = args[0]).is_a?(Symbol) || (!(value = args[1]).is_a?(Symbol) && !value.nil?)
|
189
|
+
super # Revert to normal hash behaviour when we're not passed symbols
|
190
|
+
else
|
191
|
+
case (current = fetch(key, nil))
|
192
|
+
when value
|
193
|
+
if value.nil? # Setting a single value where nothing yet exists
|
194
|
+
case orig_parent
|
195
|
+
when ::Brick::JoinHash
|
196
|
+
if self.empty? # Convert this empty hash into a JoinArray
|
197
|
+
orig_parent._brick_store(parent_key, ::Brick::JoinArray.new.replace([key]))
|
198
|
+
else # Call back into []= to use our own logic, this time setting this value from the context of the parent
|
199
|
+
orig_parent[parent_key] = key
|
200
|
+
end
|
201
|
+
when ::Brick::JoinArray
|
202
|
+
orig_parent[parent_key][key] = nil
|
203
|
+
else # No knowledge of any parent, so all we can do is add this single value right here as { key => nil }
|
204
|
+
super
|
205
|
+
end
|
206
|
+
key
|
207
|
+
else # Setting a key / value pair where nothing yet exists
|
208
|
+
puts "X2"
|
209
|
+
super(key, ::Brick::JoinArray.new.replace([value]))
|
210
|
+
value
|
211
|
+
end
|
212
|
+
when Symbol # Upgrade an existing symbol to be a part of our special JoinArray
|
213
|
+
puts "X3"
|
214
|
+
super(key, ::Brick::JoinArray.new.replace([current, value]))
|
215
|
+
when ::Brick::JoinArray # Concatenate new stuff onto any existing JoinArray
|
216
|
+
current.set_matching(value, nil)
|
217
|
+
when ::Brick::JoinHash # Graduate an existing hash into being in an array if things are dissimilar
|
218
|
+
super(key, ::Brick::JoinArray.new.replace([current, value]))
|
219
|
+
value
|
220
|
+
else # Perhaps this is part of some hybrid thing
|
221
|
+
super(key, ::Brick::JoinArray.new.replace([value]))
|
222
|
+
value
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
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
|
+
# So that we can map an association name to any special alias name used in an AREL query
|
107
|
+
ans = (model._assoc_names[a.name] ||= [])
|
108
|
+
next if !const_defined?(a.name.to_s.singularize.camelize) && ::Brick.config.exclude_tables.include?(a.plural_name)
|
109
|
+
|
110
|
+
ans << a.klass unless ans.include?(a.klass)
|
106
111
|
case a.macro
|
107
112
|
when :belongs_to
|
108
113
|
s.first[a.foreign_key] = [a.name, a.klass]
|
109
|
-
when :has_many, :has_one
|
114
|
+
when :has_many, :has_one # This gets has_many as well as has_many :through
|
115
|
+
# %%% weed out ones that don't have an available model to reference
|
110
116
|
s.last[a.name] = a
|
111
117
|
end
|
112
118
|
end
|
119
|
+
# Mark has_manys that go to an associative ("join") table so that they are skipped in the UI,
|
120
|
+
# as well as any possible polymorphic associations
|
121
|
+
skip_hms = {}
|
122
|
+
associatives = hms.each_with_object({}) do |hmt, s|
|
123
|
+
if (through = hmt.last.options[:through])
|
124
|
+
skip_hms[through] = nil
|
125
|
+
s[hmt.first] = hms[through] # End up with a hash of HMT names pointing to join-table associations
|
126
|
+
elsif hmt.last.inverse_of.nil?
|
127
|
+
puts "SKIPPING #{hmt.last.name.inspect}"
|
128
|
+
# %%% If we don't do this then below associative.name will find that associative is nil
|
129
|
+
skip_hms[hmt.last.name] = nil
|
130
|
+
end
|
131
|
+
end
|
132
|
+
skip_hms.each do |k, _v|
|
133
|
+
puts hms.delete(k).inspect
|
134
|
+
end
|
135
|
+
[bts, hms, associatives]
|
113
136
|
end
|
114
137
|
|
115
138
|
# Switches Brick auto-models on or off, for all threads
|
@@ -58,24 +58,24 @@ module Brick
|
|
58
58
|
end
|
59
59
|
|
60
60
|
bar = case possible_additional_references.length
|
61
|
-
|
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.)
|
@@ -159,7 +159,7 @@ module Brick
|
|
159
159
|
# Brick.default_route_fallback = 'customers' # This defaults to \"customers/index\"
|
160
160
|
# Brick.default_route_fallback = 'orders/outstanding' # Example of a non-RESTful route
|
161
161
|
# Brick.default_route_fallback = '' # Omits setting a default route in the absence of any other
|
162
|
-
"
|
162
|
+
")
|
163
163
|
end
|
164
164
|
end
|
165
165
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: brick
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.20
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lorin Thwaits
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-04-
|
11
|
+
date: 2022-04-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -221,6 +221,7 @@ files:
|
|
221
221
|
- lib/brick/frameworks/rails/controller.rb
|
222
222
|
- lib/brick/frameworks/rails/engine.rb
|
223
223
|
- lib/brick/frameworks/rspec.rb
|
224
|
+
- lib/brick/join_array.rb
|
224
225
|
- lib/brick/serializers/json.rb
|
225
226
|
- lib/brick/serializers/yaml.rb
|
226
227
|
- lib/brick/util.rb
|