brick 0.1.0 → 1.0.2

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 36447b749972f091f6d4c7c9127a7abfd5291d159f4ab81e0fb423feae7c2c03
4
+ data.tar.gz: eb2d1c36e67dd692ac765198c132c801c8633e32ecd199f42605f7659e7f3abd
5
+ SHA512:
6
+ metadata.gz: da45315cae612125917448549f3be6cf163c6dc30ac97794f57544321b2c21536600903e9eb30a379fc6a0845656fe7704df863db13ad62a70c623f4f9cba454
7
+ data.tar.gz: 64ec86f256752004d07447e94788e453f3e6cf9006082629f5e46791b36d6ce8031e22753217d5f23be5b672f65b35ddb9a55b696db5de0bb85686f854154fee
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+ require 'brick/serializers/yaml'
5
+
6
+ module Brick
7
+ # Global configuration affecting all threads. Some thread-specific
8
+ # configuration can be found in `brick.rb`, others in `controller.rb`.
9
+ class Config
10
+ include Singleton
11
+ attr_accessor :serializer, :version_limit, :association_reify_error_behaviour,
12
+ :object_changes_adapter, :root_model
13
+
14
+ def initialize
15
+ # Variables which affect all threads, whose access is synchronized.
16
+ @mutex = Mutex.new
17
+ @enabled = true
18
+
19
+ # Variables which affect all threads, whose access is *not* synchronized.
20
+ @serializer = Brick::Serializers::YAML
21
+ end
22
+
23
+ # Indicates whether Brick models are on or off. Default: true.
24
+ def enable_models
25
+ @mutex.synchronize { !!@enable_models }
26
+ end
27
+
28
+ def enable_models=(enable)
29
+ @mutex.synchronize { @enable_models = enable }
30
+ end
31
+
32
+ # Indicates whether Brick controllers are on or off. Default: true.
33
+ def enable_controllers
34
+ @mutex.synchronize { !!@enable_controllers }
35
+ end
36
+
37
+ def enable_controllers=(enable)
38
+ @mutex.synchronize { @enable_controllers = enable }
39
+ end
40
+
41
+ # Indicates whether Brick views are on or off. Default: true.
42
+ def enable_views
43
+ @mutex.synchronize { !!@enable_views }
44
+ end
45
+
46
+ def enable_views=(enable)
47
+ @mutex.synchronize { @enable_views = enable }
48
+ end
49
+
50
+ # Indicates whether Brick routes are on or off. Default: true.
51
+ def enable_routes
52
+ @mutex.synchronize { !!@enable_routes }
53
+ end
54
+
55
+ def enable_routes=(enable)
56
+ @mutex.synchronize { @enable_routes = enable }
57
+ end
58
+
59
+ # Additional table associations to use (Think of these as virtual foreign keys perhaps)
60
+ def additional_references
61
+ @mutex.synchronize { @additional_references }
62
+ end
63
+
64
+ def additional_references=(references)
65
+ @mutex.synchronize { @additional_references = references }
66
+ end
67
+
68
+ def skip_database_views
69
+ @mutex.synchronize { @skip_database_views }
70
+ end
71
+
72
+ def skip_database_views=(disable)
73
+ @mutex.synchronize { @skip_database_views = disable }
74
+ end
75
+
76
+ def exclude_tables
77
+ @mutex.synchronize { @exclude_tables }
78
+ end
79
+
80
+ def exclude_tables=(value)
81
+ @mutex.synchronize { @exclude_tables = value }
82
+ end
83
+
84
+ def metadata_columns
85
+ @mutex.synchronize { @metadata_columns }
86
+ end
87
+
88
+ def metadata_columns=(columns)
89
+ @mutex.synchronize { @metadata_columns = columns }
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,432 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Have markers on HM relationships to indicate "load this one every time" or "lazy load it" or "don't bother"
4
+ # Others on BT to indicate "this is a lookup"
5
+
6
+ # Mark specific tables as being lookups and they get put on the main screen as an editable thing
7
+ # If they relate to multiple different things (like looking up countries or something) then they only get edited from the main page, and importing new addresses can create a new country if needed.
8
+ # Indications of how relationships should operate will be useful soon (lookup is one kind, but probably more other kinds like this stuff makes a table or makes a list or who knows what.)
9
+ # Security must happen now -- at the model level, really low AR level automatically applied.
10
+
11
+ # Similar to .includes or .joins or something, bring in all records related through a HM, and include them in a trim way in a block of JSON
12
+ # Javascript thing that automatically makes nested table things from a block of hierarchical data (maybe sorta use one dimension of the crosstab thing)
13
+
14
+ # Finally incorporate the crosstab so that many dimensions can be set up as columns or rows and be made editable.
15
+
16
+ # X or Y axis can be made as driven by either columns or a row of data, so traditional table or crosstab can be shown, or a hybrid kind of thing of the two.
17
+
18
+ # Sensitive stuff -- make a lock icon thing so people don't accidentally edit stuff
19
+
20
+ # Static text that can go on pages - headings and footers and whatever
21
+ # Eventually some indication about if it should be a paginated table / unpaginated / a list of just some fields / etc
22
+
23
+ # Grid where each cell is one field and then when you mouse over then it shows a popup other table of detail inside
24
+
25
+ # DSL that describes the rows / columns and then what each cell can have, which could be nested related data, the specifics of X and Y driving things in the cell definition like a formula
26
+
27
+ # colour coded origins
28
+
29
+ # Drag TmfModel#name onto the rows and have it automatically add five columns -- where type=zone / where type = sectionn / etc
30
+
31
+ # ==========================================================
32
+ # Dynamically create model or controller classes when needed
33
+ # ==========================================================
34
+
35
+ # By default all models indicate that they are not views
36
+ module ActiveRecord
37
+ class Base
38
+ def self.is_view?
39
+ false
40
+ end
41
+
42
+ # Used to show a little prettier name for an object
43
+ def brick_descrip
44
+ klass = self.class
45
+ klass.primary_key ? "#{klass.name} ##{send(klass.primary_key)}" : to_s
46
+ end
47
+
48
+ private
49
+
50
+ def self._brick_get_fks
51
+ @_brick_get_fks ||= reflect_on_all_associations.select { |a2| a2.macro == :belongs_to }.map(&:foreign_key)
52
+ end
53
+ end
54
+
55
+ class Relation
56
+ def brick_where(params)
57
+ wheres = {}
58
+ params.each do |k, v|
59
+ next unless klass._brick_get_fks.include?(k)
60
+
61
+ wheres[k] = v.split(',')
62
+ end
63
+ unless wheres.empty?
64
+ where!(wheres)
65
+ wheres # Return the specific parameters that we did use
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ # Object.class_exec do
72
+ class Object
73
+ class << self
74
+ alias _brick_const_missing const_missing
75
+ def const_missing(*args)
76
+ return Object.const_get(args.first) if Object.const_defined?(args.first)
77
+
78
+ class_name = args.first.to_s
79
+ # See if a file is there in the same way that ActiveSupport::Dependencies#load_missing_constant
80
+ # checks for it in ~/.rvm/gems/ruby-2.7.5/gems/activesupport-5.2.6.2/lib/active_support/dependencies.rb
81
+ # that is, checking #qualified_name_for with: from_mod, const_name
82
+ # If we want to support namespacing in the future, might have to utilise something like this:
83
+ # path_suffix = ActiveSupport::Dependencies.qualified_name_for(Object, args.first).underscore
84
+ # return Object._brick_const_missing(*args) if ActiveSupport::Dependencies.search_for_file(path_suffix)
85
+ # If the file really exists, go and snag it:
86
+ return Object._brick_const_missing(*args) if ActiveSupport::Dependencies.search_for_file(class_name.underscore)
87
+
88
+ relations = ::Brick.instance_variable_get(:@relations)[ActiveRecord::Base.connection_pool.object_id] || {}
89
+ result = if ::Brick.enable_controllers? && class_name.end_with?('Controller') && (plural_class_name = class_name[0..-11]).length.positive?
90
+ # Otherwise now it's up to us to fill in the gaps
91
+ if (model = ActiveSupport::Inflector.singularize(plural_class_name).constantize)
92
+ # 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.
93
+ build_controller(class_name, plural_class_name, model, relations)
94
+ end
95
+ elsif ::Brick.enable_models?
96
+ # See if a file is there in the same way that ActiveSupport::Dependencies#load_missing_constant
97
+ # checks for it in ~/.rvm/gems/ruby-2.7.5/gems/activesupport-5.2.6.2/lib/active_support/dependencies.rb
98
+ plural_class_name = ActiveSupport::Inflector.pluralize(model_name = class_name)
99
+ singular_table_name = ActiveSupport::Inflector.underscore(model_name)
100
+ table_name = ActiveSupport::Inflector.pluralize(singular_table_name)
101
+
102
+ # Maybe, just maybe there's a database table that will satisfy this need
103
+ if (matching = [table_name, singular_table_name, plural_class_name, model_name].find { |m| relations.key?(m) })
104
+ build_model(model_name, singular_table_name, table_name, relations, matching)
105
+ end
106
+ end
107
+ if result
108
+ built_class, code = result
109
+ puts "\n#{code}"
110
+ built_class
111
+ else
112
+ puts "MISSING! #{args.inspect} #{table_name}"
113
+ Object._brick_const_missing(*args)
114
+ end
115
+ end
116
+
117
+ private
118
+
119
+ def build_model(model_name, singular_table_name, table_name, relations, matching)
120
+ return if ((is_view = (relation = relations[matching]).key?(:isView)) && ::Brick.config.skip_database_views) ||
121
+ ::Brick.config.exclude_tables.include?(matching)
122
+
123
+ # Are they trying to use a pluralised class name such as "Employees" instead of "Employee"?
124
+ if table_name == singular_table_name && !ActiveSupport::Inflector.inflections.uncountable.include?(table_name)
125
+ raise NameError.new("Class name for a model that references table \"#{matching}\" should be \"#{ActiveSupport::Inflector.singularize(model_name)}\".")
126
+ end
127
+ code = +"class #{model_name} < ActiveRecord::Base\n"
128
+ built_model = Class.new(ActiveRecord::Base) do |new_model_class|
129
+ Object.const_set(model_name.to_sym, new_model_class)
130
+ # Accommodate singular or camel-cased table names such as "order_detail" or "OrderDetails"
131
+ code << " self.table_name = '#{self.table_name = matching}'\n" unless table_name == matching
132
+
133
+ # Override models backed by a view so they return true for #is_view?
134
+ # (Dynamically-created controllers and view templates for such models will then act in a read-only way)
135
+ if is_view
136
+ new_model_class.define_singleton_method :'is_view?' do
137
+ true
138
+ end
139
+ code << " def self.is_view?; true; end\n"
140
+ end
141
+
142
+ # Missing a primary key column? (Usually "id")
143
+ ar_pks = primary_key.is_a?(String) ? [primary_key] : primary_key || []
144
+ db_pks = relation[:cols]&.map(&:first)
145
+ has_pk = ar_pks.length.positive? && (db_pks & ar_pks).sort == ar_pks.sort
146
+ our_pks = relation[:pkey].values.first
147
+ # No primary key, but is there anything UNIQUE?
148
+ # (Sort so that if there are multiple UNIQUE constraints we'll pick one that uses the least number of columns.)
149
+ our_pks = relation[:ukeys].values.sort { |a, b| a.length <=> b.length }.first unless our_pks&.present?
150
+ if has_pk
151
+ code << " # Primary key: #{ar_pks.join(', ')}\n" unless ar_pks == ['id']
152
+ elsif our_pks&.present?
153
+ if our_pks.length > 1 && respond_to?(:'primary_keys=') # Using the composite_primary_keys gem?
154
+ new_model_class.primary_keys = our_pks
155
+ code << " self.primary_keys = #{our_pks.map(&:to_sym).inspect}\n"
156
+ else
157
+ new_model_class.primary_key = (pk_sym = our_pks.first.to_sym)
158
+ code << " self.primary_key = #{pk_sym.inspect}\n"
159
+ end
160
+ else
161
+ code << " # Could not identify any column(s) to use as a primary key\n" unless is_view
162
+ end
163
+
164
+ # if relation[:cols].key?('last_update')
165
+ # define_method :updated_at do
166
+ # last_update
167
+ # end
168
+ # define_method :'updated_at=' do |val|
169
+ # last_update=(val)
170
+ # end
171
+ # end
172
+
173
+ fks = relation[:fks] || {}
174
+ fks.each do |_constraint_name, assoc|
175
+ assoc_name = assoc[:assoc_name]
176
+ inverse_assoc_name = assoc[:inverse][:assoc_name]
177
+ options = {}
178
+ singular_table_name = ActiveSupport::Inflector.singularize(assoc[:inverse_table])
179
+ macro = if assoc[:is_bt]
180
+ need_class_name = singular_table_name.underscore != assoc_name
181
+ need_fk = "#{assoc_name}_id" != assoc[:fk]
182
+ inverse_assoc_name, _x = _brick_get_hm_assoc_name(relations[assoc[:inverse_table]], assoc[:inverse])
183
+ :belongs_to
184
+ else
185
+ # need_class_name = ActiveSupport::Inflector.singularize(assoc_name) == ActiveSupport::Inflector.singularize(table_name.underscore)
186
+ # Are there multiple foreign keys out to the same table?
187
+ assoc_name, need_class_name = _brick_get_hm_assoc_name(relation, assoc)
188
+ need_fk = "#{singular_table_name}_id" != assoc[:fk]
189
+ # fks[table_name].find { |other_assoc| other_assoc.object_id != assoc.object_id && other_assoc[:assoc_name] == assoc[assoc_name] }
190
+ :has_many
191
+ end
192
+ options[:class_name] = singular_table_name.camelize if need_class_name
193
+ # Figure out if we need to specially call out the foreign key
194
+ if need_fk # Funky foreign key?
195
+ options[:foreign_key] = assoc[:fk].to_sym
196
+ end
197
+ options[:inverse_of] = inverse_assoc_name.to_sym if need_class_name || need_fk
198
+ assoc_name = assoc_name.to_sym
199
+ code << " #{macro} #{assoc_name.inspect}#{options.map { |k, v| ", #{k}: #{v.inspect}" }.join}\n"
200
+ self.send(macro, assoc_name, **options)
201
+
202
+ # Look for any valid "has_many :through"
203
+ if macro == :has_many
204
+ relations[assoc[:inverse_table]][:hmt_fks].each do |k, hmt_fk|
205
+ next if k == assoc[:fk]
206
+
207
+ hmt_fk = ActiveSupport::Inflector.pluralize(hmt_fk)
208
+ code << " has_many :#{hmt_fk}, through: #{assoc_name.inspect}\n"
209
+ self.send(:has_many, hmt_fk.to_sym, **{ through: assoc_name })
210
+ end
211
+ end
212
+ end
213
+ code << "end # model #{model_name}\n\n"
214
+ end # class definition
215
+ [built_model, code]
216
+ end
217
+
218
+ def build_controller(class_name, plural_class_name, model, relations)
219
+ table_name = ActiveSupport::Inflector.underscore(plural_class_name)
220
+ singular_table_name = ActiveSupport::Inflector.singularize(table_name)
221
+
222
+ code = +"class #{class_name} < ApplicationController\n"
223
+ built_controller = Class.new(ActionController::Base) do |new_controller_class|
224
+ Object.const_set(class_name.to_sym, new_controller_class)
225
+
226
+ code << " def index\n"
227
+ code << " @#{table_name} = #{model.name}#{model.primary_key ? ".order(#{model.primary_key.inspect}" : '.all'})\n"
228
+ code << " @#{table_name}.brick_where(params)\n"
229
+ code << " end\n"
230
+ self.define_method :index do
231
+ ar_relation = model.primary_key ? model.order(model.primary_key) : model.all
232
+ instance_variable_set(:@_brick_params, ar_relation.brick_where(params))
233
+ instance_variable_set("@#{table_name}".to_sym, ar_relation)
234
+ end
235
+
236
+ if model.primary_key
237
+ code << " def show\n"
238
+ code << " @#{singular_table_name} = #{model.name}.find(params[:id].split(','))\n"
239
+ code << " end\n"
240
+ self.define_method :show do
241
+ instance_variable_set("@#{singular_table_name}".to_sym, model.find(params[:id].split(',')))
242
+ end
243
+ end
244
+
245
+ # By default, views get marked as read-only
246
+ unless (relation = relations[model.table_name]).key?(:isView)
247
+ code << " # (Define :new, :create, :edit, :update, and :destroy)\n"
248
+ # Get column names for params from relations[model.table_name][:cols].keys
249
+ end
250
+ code << "end # #{class_name}\n\n"
251
+ end # class definition
252
+ [built_controller, code]
253
+ end
254
+
255
+ def _brick_get_hm_assoc_name(relation, hm_assoc)
256
+ if relation[:hm_counts][hm_assoc[:assoc_name]] > 1
257
+ [ActiveSupport::Inflector.pluralize(hm_assoc[:alternate_name]), true]
258
+ else
259
+ [ActiveSupport::Inflector.pluralize(hm_assoc[:inverse_table]), nil]
260
+ end
261
+ end
262
+ end
263
+ end
264
+
265
+ # ==========================================================
266
+ # Get info on all relations during first database connection
267
+ # ==========================================================
268
+
269
+ module ActiveRecord::ConnectionHandling
270
+ alias _brick_establish_connection establish_connection
271
+ def establish_connection(*args)
272
+ x = _brick_establish_connection(*args)
273
+
274
+ if (relations = ::Brick.relations).empty?
275
+ schema = 'public'
276
+ puts ActiveRecord::Base.connection.execute("SELECT current_setting('SEARCH_PATH')").to_a.inspect
277
+ sql = ActiveRecord::Base.send(:sanitize_sql_array, [
278
+ "SELECT t.table_name AS relation_name, t.table_type,
279
+ c.column_name, c.data_type,
280
+ COALESCE(c.character_maximum_length, c.numeric_precision) AS max_length,
281
+ tc.constraint_type AS const, kcu.constraint_name AS key
282
+ FROM INFORMATION_SCHEMA.tables AS t
283
+ LEFT OUTER JOIN INFORMATION_SCHEMA.columns AS c ON t.table_schema = c.table_schema
284
+ AND t.table_name = c.table_name
285
+ LEFT OUTER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu ON
286
+ -- ON kcu.CONSTRAINT_CATALOG = t.table_catalog AND
287
+ kcu.CONSTRAINT_SCHEMA = c.table_schema
288
+ AND kcu.TABLE_NAME = c.table_name
289
+ AND kcu.position_in_unique_constraint IS NULL
290
+ AND kcu.ordinal_position = c.ordinal_position
291
+ LEFT OUTER JOIN INFORMATION_SCHEMA.table_constraints AS tc
292
+ ON kcu.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA
293
+ AND kcu.CONSTRAINT_NAME = tc.constraint_name
294
+ WHERE t.table_schema = ? -- COALESCE(current_setting('SEARCH_PATH'), 'public')
295
+ -- AND t.table_type IN ('VIEW') -- 'BASE TABLE', 'FOREIGN TABLE'
296
+ AND t.table_name NOT IN ('pg_stat_statements', 'ar_internal_metadata', 'schema_migrations')
297
+ ORDER BY 1, t.table_type DESC, c.ordinal_position", schema
298
+ ])
299
+ ActiveRecord::Base.connection.execute(sql).each do |r|
300
+ # next if internal_views.include?(r['relation_name']) # Skip internal views such as v_all_assessments
301
+
302
+ relation = relations[r['relation_name']]
303
+ relation[:isView] = true if r['table_type'] == 'VIEW'
304
+ col_name = r['column_name']
305
+ cols = relation[:cols] # relation.fetch(:cols) { relation[:cols] = [] }
306
+ key = case r['const']
307
+ when 'PRIMARY KEY'
308
+ relation[:pkey][r['key']] ||= []
309
+ when 'UNIQUE'
310
+ relation[:ukeys][r['key']] ||= []
311
+ # key = (relation[:ukeys] = Hash.new { |h, k| h[k] = [] }) if key.is_a?(Array)
312
+ # key[r['key']]
313
+ end
314
+ key << col_name if key
315
+ cols[col_name] = [r['data_type'], r['max_length'], r['measures']&.include?(col_name)]
316
+ # puts "KEY! #{r['relation_name']}.#{col_name} #{r['key']} #{r['const']}" if r['key']
317
+ end
318
+
319
+ sql = ActiveRecord::Base.send(:sanitize_sql_array, [
320
+ "SELECT kcu1.TABLE_NAME, kcu1.COLUMN_NAME, kcu2.TABLE_NAME, kcu1.CONSTRAINT_NAME
321
+ FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS rc
322
+ INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu1
323
+ ON kcu1.CONSTRAINT_CATALOG = rc.CONSTRAINT_CATALOG
324
+ AND kcu1.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA
325
+ AND kcu1.CONSTRAINT_NAME = rc.CONSTRAINT_NAME
326
+ INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu2
327
+ ON kcu2.CONSTRAINT_CATALOG = rc.UNIQUE_CONSTRAINT_CATALOG
328
+ AND kcu2.CONSTRAINT_SCHEMA = rc.UNIQUE_CONSTRAINT_SCHEMA
329
+ AND kcu2.CONSTRAINT_NAME = rc.UNIQUE_CONSTRAINT_NAME
330
+ AND kcu2.ORDINAL_POSITION = kcu1.ORDINAL_POSITION
331
+ WHERE kcu1.CONSTRAINT_SCHEMA = ? -- COALESCE(current_setting('SEARCH_PATH'), 'public')", schema
332
+ # AND kcu2.TABLE_NAME = ?;", Apartment::Tenant.current, table_name
333
+ ])
334
+ ActiveRecord::Base.connection.execute(sql).values.each { |fk| ::Brick._add_bt_and_hm(fk, relations) }
335
+ end
336
+
337
+ puts "Classes built from tables:"
338
+ relations.select { |_k, v| !v.key?(:isView) }.keys.each { |k| puts ActiveSupport::Inflector.singularize(k).camelize }
339
+ puts "Classes built from views:"
340
+ relations.select { |_k, v| v.key?(:isView) }.keys.each { |k| puts ActiveSupport::Inflector.singularize(k).camelize }
341
+ # pp relations; nil
342
+
343
+ # relations.keys.each { |k| ActiveSupport::Inflector.singularize(k).camelize.constantize }
344
+ # Layout table describes permissioned hierarchy throughout
345
+ x
346
+ end
347
+ end
348
+
349
+ # ==========================================
350
+
351
+ # :nodoc:
352
+ module Brick
353
+ # rubocop:disable Style/CommentedKeyword
354
+ module Extensions
355
+ MAX_ID = Arel.sql('MAX(id)')
356
+ IS_AMOEBA = Gem.loaded_specs['amoeba']
357
+
358
+ def self.included(base)
359
+ base.send :extend, ClassMethods
360
+ end
361
+
362
+ # :nodoc:
363
+ module ClassMethods
364
+
365
+ private
366
+
367
+ end
368
+ end # module Extensions
369
+ # rubocop:enable Style/CommentedKeyword
370
+
371
+ def self._add_bt_and_hm(fk, relations = nil)
372
+ relations ||= ::Brick.relations
373
+ bt_assoc_name = fk[1].underscore
374
+ bt_assoc_name = bt_assoc_name[0..-4] if bt_assoc_name.end_with?('_id')
375
+
376
+ bts = (relation = relations.fetch(fk[0], nil))&.fetch(:fks) { relation[:fks] = {} }
377
+ hms = (relation = relations.fetch(fk[2], nil))&.fetch(:fks) { relation[:fks] = {} }
378
+
379
+ unless (cnstr_name = fk[3])
380
+ # For any appended references (those that come from config), arrive upon a definitely unique constraint name
381
+ cnstr_base = cnstr_name = "(brick) #{fk[0]}_#{fk[2]}"
382
+ cnstr_added_num = 1
383
+ cnstr_name = "#{cnstr_base}_#{cnstr_added_num += 1}" while bts&.key?(cnstr_name) || hms&.key?(cnstr_name)
384
+ missing = []
385
+ missing << fk[0] unless relations.key?(fk[0])
386
+ missing << fk[2] unless relations.key?(fk[2])
387
+ unless missing.empty?
388
+ tables = relations.reject { |k, v| v.fetch(:isView, nil) }.keys.sort
389
+ puts "Brick: Additional reference #{fk.inspect} refers to non-existent #{'table'.pluralize(missing.length)} #{missing.join(' and ')}. (Available tables include #{tables.join(', ')}.)"
390
+ return
391
+ end
392
+ unless (cols = relations[fk[0]][:cols]).key?(fk[1])
393
+ columns = cols.map { |k, v| "#{k} (#{v.first.split(' ').first})" }
394
+ puts "Brick: Additional reference #{fk.inspect} refers to non-existent column #{fk[1]}. (Columns present in #{fk[0]} are #{columns.join(', ')}.)"
395
+ return
396
+ end
397
+ if (redundant = bts.find{|k, v| v[:inverse][:inverse_table] == fk[0] && v[:fk] == fk[1] && v[:inverse_table] == fk[2] })
398
+ puts "Brick: Additional reference #{fk.inspect} is redundant and can be removed. (Already established by #{redundant.first}.)"
399
+ return
400
+ end
401
+ end
402
+
403
+ if (assoc_bt = bts[cnstr_name])
404
+ assoc_bt[:fk] = assoc_bt[:fk].is_a?(String) ? [assoc_bt[:fk], fk[1]] : assoc_bt[:fk].concat(fk[1])
405
+ assoc_bt[:assoc_name] = "#{assoc_bt[:assoc_name]}_#{fk[1]}"
406
+ else
407
+ assoc_bt = bts[cnstr_name] = { is_bt: true, fk: fk[1], assoc_name: bt_assoc_name, inverse_table: fk[2] }
408
+ end
409
+
410
+ if (assoc_hm = hms[cnstr_name])
411
+ assoc_hm[:fk] = assoc_hm[:fk].is_a?(String) ? [assoc_hm[:fk], fk[1]] : assoc_hm[:fk].concat(fk[1])
412
+ assoc_hm[:alternate_name] = "#{assoc_hm[:alternate_name]}_#{bt_assoc_name}" unless assoc_hm[:alternate_name] == bt_assoc_name
413
+ assoc_hm[:inverse] = assoc_bt
414
+ else
415
+ assoc_hm = hms[cnstr_name] = { is_bt: false, fk: fk[1], assoc_name: fk[0], alternate_name: bt_assoc_name, inverse_table: fk[0], inverse: assoc_bt }
416
+ hm_counts = relation.fetch(:hm_counts) { relation[:hm_counts] = {} }
417
+ hm_counts[fk[0]] = hm_counts.fetch(fk[0]) { 0 } + 1
418
+ end
419
+ assoc_bt[:inverse] = assoc_hm
420
+ # hms[cnstr_name] << { is_bt: false, fk: fk[1], assoc_name: fk[0], alternate_name: bt_assoc_name, inverse_table: fk[0] }
421
+ end
422
+
423
+ # Rails < 4.0 doesn't have ActiveRecord::RecordNotUnique, so use the more generic ActiveRecord::ActiveRecordError instead
424
+ ar_not_unique_error = ActiveRecord.const_defined?('RecordNotUnique') ? ActiveRecord::RecordNotUnique : ActiveRecord::ActiveRecordError
425
+ class NoUniqueColumnError < ar_not_unique_error
426
+ end
427
+
428
+ # Rails < 4.2 doesn't have ActiveRecord::RecordInvalid, so use the more generic ActiveRecord::ActiveRecordError instead
429
+ ar_invalid_error = ActiveRecord.const_defined?('RecordInvalid') ? ActiveRecord::RecordInvalid : ActiveRecord::ActiveRecordError
430
+ class LessThanHalfAreMatchingColumnsError < ar_invalid_error
431
+ end
432
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ # before hook for Cucumber
4
+ Before do
5
+ # Brick.enable_routes = true
6
+ # Brick.enable_models = true
7
+ # Brick.enable_controllers = true
8
+ # Brick.enable_views = true
9
+ Brick.request.whodunnit = nil
10
+ Brick.request.controller_info = {} if defined?(::Rails)
11
+ end
12
+
13
+ module Brick
14
+ module Cucumber
15
+ # Helper method for disabling Brick in Cucumber features
16
+ module Extensions
17
+ def without_brick
18
+ was_enable_routes = ::Brick.enable_routes?
19
+ # was_enable_models = ::Brick.enable_models?
20
+ # was_enable_controllers = ::Brick.enable_controllers?
21
+ # was_enable_views = ::Brick.enable_views?
22
+ ::Brick.enable_routes = false
23
+ # ::Brick.enable_models = false
24
+ # ::Brick.enable_controllers = false
25
+ # ::Brick.enable_views = false
26
+ begin
27
+ yield
28
+ ensure
29
+ ::Brick.enable_routes = was_enable_routes
30
+ # ::Brick.enable_models = was_enable_models
31
+ # ::Brick.enable_controllers = was_enable_controllers
32
+ # ::Brick.enable_views = was_enable_views
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ World Brick::Cucumber::Extensions
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Brick
4
+ module Rails
5
+ # Extensions to rails controllers. Provides convenient ways to pass certain
6
+ # information to the model layer, with `controller_info` and `whodunnit`.
7
+ # Also includes a convenient on/off switch,
8
+ # `brick_enabled_for_controller`.
9
+ module Controller
10
+ def self.included(controller)
11
+ controller.before_action(
12
+ :set_brick_enabled_for_controller,
13
+ :set_brick_controller_info
14
+ )
15
+ end
16
+
17
+ protected
18
+
19
+ # Returns the user who is responsible for any changes that occur.
20
+ # By default this calls `current_user` and returns the result.
21
+ #
22
+ # Override this method in your controller to call a different
23
+ # method, e.g. `current_person`, or anything you like.
24
+ #
25
+ # @api public
26
+ def user_for_brick
27
+ return unless defined?(current_user)
28
+
29
+ ActiveSupport::VERSION::MAJOR >= 4 ? current_user.try!(:id) : current_user.try(:id)
30
+ rescue NoMethodError
31
+ current_user
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ if defined?(::ActionController)
38
+ ::ActiveSupport.on_load(:action_controller) do
39
+ include ::Brick::Rails::Controller
40
+ end
41
+ end