brick 1.0.19 → 1.0.20
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/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
|