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 +7 -0
- data/lib/brick/config.rb +92 -0
- data/lib/brick/extensions.rb +432 -0
- data/lib/brick/frameworks/cucumber.rb +39 -0
- data/lib/brick/frameworks/rails/controller.rb +41 -0
- data/lib/brick/frameworks/rails/engine.rb +211 -0
- data/lib/brick/frameworks/rails.rb +4 -0
- data/lib/brick/frameworks/rspec.rb +18 -0
- data/lib/brick/serializers/json.rb +36 -0
- data/lib/brick/serializers/yaml.rb +26 -0
- data/lib/brick/util.rb +123 -0
- data/lib/brick/version_number.rb +19 -0
- data/lib/brick.rb +528 -0
- data/lib/generators/brick/USAGE +2 -0
- data/lib/generators/brick/install_generator.rb +142 -0
- data/lib/generators/brick/model_generator.rb +117 -0
- data/lib/generators/brick/templates/add_object_changes_to_versions.rb.erb +12 -0
- data/lib/generators/brick/templates/create_versions.rb.erb +36 -0
- metadata +195 -48
- data/bin/brick +0 -15
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
|
data/lib/brick/config.rb
ADDED
@@ -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
|