brick 0.1.0 → 1.0.0
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 +32 -0
- data/lib/brick/extensions.rb +424 -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 +20 -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 +470 -0
- data/lib/generators/brick/USAGE +2 -0
- data/lib/generators/brick/install_generator.rb +96 -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: 580f34a900e392172361e334170169b30c63f83e96bd7b3119e9d957d440d6b1
|
4
|
+
data.tar.gz: b02a7265b8010fe06d9599d353f7cdaf2435f872411fe8d6871fbd79e275ce19
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: '038902e9a8bc77505c28cf0038ff6aa77d0440f389bbc43b29ffaccae4e344e37d1d8fa7a683aae44c5c36d6b51f282fcd22174539c47c8f30e162b3e9dc71b9'
|
7
|
+
data.tar.gz: f8e00496a7af527e47d75ef8a94cb02994224fa7fe536056b4399aac7758fdd3d3f3eddc364cbf6aec30efd4a56b852fa9982b7e06a48261a6bc9a178bc161fb
|
data/lib/brick/config.rb
ADDED
@@ -0,0 +1,32 @@
|
|
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 routes are on or off. Default: true.
|
24
|
+
def enable_routes
|
25
|
+
@mutex.synchronize { !!@enable_routes }
|
26
|
+
end
|
27
|
+
|
28
|
+
def enable_routes=(enable)
|
29
|
+
@mutex.synchronize { @enable_routes = enable }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,424 @@
|
|
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
|
+
# ====================================
|
33
|
+
# Dynamically create generic templates
|
34
|
+
# ====================================
|
35
|
+
|
36
|
+
# For templates:
|
37
|
+
class ActionView::LookupContext
|
38
|
+
alias :_brick_template_exists? :template_exists?
|
39
|
+
def template_exists?(*args, **options)
|
40
|
+
x = _brick_template_exists?(*args, **options)
|
41
|
+
# Need to return true if we can fill in the blanks for a missing one
|
42
|
+
# args will be something like: ["index", ["categories"]]
|
43
|
+
unless x
|
44
|
+
relations = Brick.instance_variable_get(:@relations)[ActiveRecord::Base.connection_pool.object_id] || {}
|
45
|
+
matching = [views_name = args[1]&.first, views_name.singularize].find { |m| relations.key?(m) }
|
46
|
+
if (x = matching && (matching = relations[matching]) &&
|
47
|
+
(['index', 'show'].include?(args.first) || # Everything has index and show
|
48
|
+
# Only CRU stuff has create / update / destroy
|
49
|
+
(!matching.key?(:isView) && ['new', 'create', 'edit', 'update', 'destroy'].include?(args.first))
|
50
|
+
)
|
51
|
+
)
|
52
|
+
instance_variable_set(:@_brick_match, matching)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
x
|
56
|
+
end
|
57
|
+
|
58
|
+
alias :_brick_find_template :find_template
|
59
|
+
def find_template(*args, **options)
|
60
|
+
if @_brick_match
|
61
|
+
inline = case args.first
|
62
|
+
when 'index'
|
63
|
+
# Something like: <%= @categories.inspect %>
|
64
|
+
"<%= @#{@_brick_match[:index]}.inspect %>"
|
65
|
+
when 'show'
|
66
|
+
"<%= @#{@_brick_match[:show]}.inspect %>"
|
67
|
+
end
|
68
|
+
# As if it were an inline template (see #determine_template in actionview-5.2.6.2/lib/action_view/renderer/template_renderer.rb)
|
69
|
+
keys = options.has_key?(:locals) ? options[:locals].keys : []
|
70
|
+
handler = ActionView::Template.handler_for_extension(options[:type] || 'erb')
|
71
|
+
ActionView::Template.new(inline, "auto-generated #{args.first} template", handler, locals: keys)
|
72
|
+
else
|
73
|
+
_brick_find_template(*args, **options)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# ==========================================================
|
79
|
+
# Dynamically create model or controller classes when needed
|
80
|
+
# ==========================================================
|
81
|
+
|
82
|
+
# Object.class_exec do
|
83
|
+
class Object
|
84
|
+
class << self
|
85
|
+
alias _brick_const_missing const_missing
|
86
|
+
def const_missing(*args)
|
87
|
+
return Object.const_get(args.first) if Object.const_defined?(args.first)
|
88
|
+
|
89
|
+
class_name = args.first.to_s
|
90
|
+
# See if a file is there in the same way that ActiveSupport::Dependencies#load_missing_constant
|
91
|
+
# checks for it in ~/.rvm/gems/ruby-2.7.5/gems/activesupport-5.2.6.2/lib/active_support/dependencies.rb
|
92
|
+
# that is, checking #qualified_name_for with: from_mod, const_name
|
93
|
+
# If we want to support namespacing in the future, might have to utilise something like this:
|
94
|
+
# path_suffix = ActiveSupport::Dependencies.qualified_name_for(Object, args.first).underscore
|
95
|
+
# return Object._brick_const_missing(*args) if ActiveSupport::Dependencies.search_for_file(path_suffix)
|
96
|
+
# If the file really exists, go and snag it:
|
97
|
+
return Object._brick_const_missing(*args) if ActiveSupport::Dependencies.search_for_file(class_name.underscore)
|
98
|
+
|
99
|
+
if class_name.end_with?('Controller') && (plural_class_name = class_name[0..-11]).length.positive?
|
100
|
+
# Otherwise now it's up to us to fill in the gaps
|
101
|
+
is_controller = true
|
102
|
+
table_name = ActiveSupport::Inflector.underscore(plural_class_name)
|
103
|
+
model_name = ActiveSupport::Inflector.singularize(plural_class_name)
|
104
|
+
singular_table_name = ActiveSupport::Inflector.singularize(table_name)
|
105
|
+
else # Model
|
106
|
+
# See if a file is there in the same way that ActiveSupport::Dependencies#load_missing_constant
|
107
|
+
# checks for it in ~/.rvm/gems/ruby-2.7.5/gems/activesupport-5.2.6.2/lib/active_support/dependencies.rb
|
108
|
+
plural_class_name = ActiveSupport::Inflector.pluralize(model_name = class_name)
|
109
|
+
singular_table_name = ActiveSupport::Inflector.underscore(model_name)
|
110
|
+
table_name = ActiveSupport::Inflector.pluralize(singular_table_name)
|
111
|
+
end
|
112
|
+
relations = Brick.instance_variable_get(:@relations)[ActiveRecord::Base.connection_pool.object_id] || {}
|
113
|
+
# Maybe, just maybe there's a database table that will satisfy this need
|
114
|
+
matches = [table_name, singular_table_name, plural_class_name, model_name]
|
115
|
+
if matching = matches.find { |m| relations.key?(m) }
|
116
|
+
built_class, code = if is_controller
|
117
|
+
build_controller(class_name, model_name, singular_table_name, table_name, relations, matching)
|
118
|
+
else
|
119
|
+
build_model(model_name, singular_table_name, table_name, relations, matching)
|
120
|
+
end
|
121
|
+
puts "#{code}end # #{ is_controller ? 'controller' : 'model' }\n\n"
|
122
|
+
built_class
|
123
|
+
else
|
124
|
+
puts "MISSING! #{args.inspect} #{table_name}"
|
125
|
+
Object._brick_const_missing(*args)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
def build_model(model_name, singular_table_name, table_name, relations, matching)
|
132
|
+
# Are they trying to use a pluralised class name such as "Employees" instead of "Employee"?
|
133
|
+
if table_name == singular_table_name && !ActiveSupport::Inflector.inflections.uncountable.include?(table_name)
|
134
|
+
raise NameError.new("Class name for a model that references table \"#{matching}\" should be \"#{ActiveSupport::Inflector.singularize(class_name)}\".")
|
135
|
+
end
|
136
|
+
code = +"class #{model_name} < ActiveRecord::Base\n"
|
137
|
+
built_model = Class.new(ActiveRecord::Base) do |new_model_class|
|
138
|
+
Object.const_set(model_name.to_sym, new_model_class)
|
139
|
+
# Accommodate singular or camel-cased table names such as "order_detail" or "OrderDetails"
|
140
|
+
code << " self.table_name = '#{self.table_name = matching}'\n" unless table_name == matching
|
141
|
+
|
142
|
+
# By default, views get marked as read-only
|
143
|
+
if (relation = relations[matching]).key?(:isView)
|
144
|
+
self.define_method :'readonly?' do
|
145
|
+
true
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Missing a primary key column? (Usually "id")
|
150
|
+
ar_pks = primary_key.is_a?(String) ? [primary_key] : primary_key || []
|
151
|
+
db_pks = relation[:cols]&.map(&:first)
|
152
|
+
has_pk = ar_pks.length.positive? && (db_pks & ar_pks).sort == ar_pks.sort
|
153
|
+
our_pks = relation[:pkey].values.first
|
154
|
+
# No primary key, but is there anything UNIQUE?
|
155
|
+
# (Sort so that if there are multiple UNIQUE constraints we'll pick one that uses the least number of columns.)
|
156
|
+
our_pks = relation[:ukeys].values.sort { |a, b| a.length <=> b.length }.first unless our_pks&.present?
|
157
|
+
if has_pk
|
158
|
+
code << " # Primary key: #{ar_pks.join(', ')}\n" unless ar_pks == ['id']
|
159
|
+
elsif our_pks&.present?
|
160
|
+
if our_pks.length > 1 && respond_to?(:'primary_keys=') # Using the composite_primary_keys gem?
|
161
|
+
new_model_class.primary_keys = our_pks
|
162
|
+
code << " self.primary_keys = #{our_pks.map(&:to_sym).inspect}\n"
|
163
|
+
else
|
164
|
+
new_model_class.primary_key = (pk_sym = our_pks.first.to_sym)
|
165
|
+
code << " self.primary_key = #{pk_sym.inspect}\n"
|
166
|
+
end
|
167
|
+
else
|
168
|
+
code << " # Could not identify any column(s) to use as a primary key\n"
|
169
|
+
end
|
170
|
+
|
171
|
+
# if relation[:cols].key?('last_update')
|
172
|
+
# define_method :updated_at do
|
173
|
+
# last_update
|
174
|
+
# end
|
175
|
+
# define_method :'updated_at=' do |val|
|
176
|
+
# last_update=(val)
|
177
|
+
# end
|
178
|
+
# end
|
179
|
+
|
180
|
+
fks = relation[:fks] || {}
|
181
|
+
fks.each do |_constraint_name, assoc|
|
182
|
+
assoc_name = assoc[:assoc_name]
|
183
|
+
inverse_assoc_name = assoc[:inverse][:assoc_name]
|
184
|
+
options = {}
|
185
|
+
singular_table_name = ActiveSupport::Inflector.singularize(assoc[:inverse_table])
|
186
|
+
macro = if assoc[:is_bt]
|
187
|
+
need_class_name = singular_table_name.underscore != assoc_name
|
188
|
+
need_fk = "#{assoc_name}_id" != assoc[:fk]
|
189
|
+
inverse_assoc_name, _x = _brick_get_hm_assoc_name(relations[assoc[:inverse_table]], assoc[:inverse])
|
190
|
+
:belongs_to
|
191
|
+
else
|
192
|
+
# need_class_name = ActiveSupport::Inflector.singularize(assoc_name) == ActiveSupport::Inflector.singularize(table_name.underscore)
|
193
|
+
# Are there multiple foreign keys out to the same table?
|
194
|
+
assoc_name, need_class_name = _brick_get_hm_assoc_name(relation, assoc)
|
195
|
+
need_fk = "#{singular_table_name}_id" != assoc[:fk]
|
196
|
+
# fks[table_name].find { |other_assoc| other_assoc.object_id != assoc.object_id && other_assoc[:assoc_name] == assoc[assoc_name] }
|
197
|
+
:has_many
|
198
|
+
end
|
199
|
+
options[:class_name] = singular_table_name.camelize if need_class_name
|
200
|
+
# Figure out if we need to specially call out the foreign key
|
201
|
+
if need_fk # Funky foreign key?
|
202
|
+
options[:foreign_key] = assoc[:fk].to_sym
|
203
|
+
end
|
204
|
+
options[:inverse_of] = inverse_assoc_name.to_sym if need_class_name || need_fk
|
205
|
+
assoc_name = assoc_name.to_sym
|
206
|
+
code << " #{macro} #{assoc_name.inspect}#{options.map { |k, v| ", #{k}: #{v.inspect}" }.join}\n"
|
207
|
+
self.send(macro, assoc_name, **options)
|
208
|
+
|
209
|
+
# Look for any valid "has_many :through"
|
210
|
+
if macro == :has_many
|
211
|
+
relations[assoc[:inverse_table]][:hmt_fks].each do |k, hmt_fk|
|
212
|
+
next if k == assoc[:fk]
|
213
|
+
|
214
|
+
hmt_fk = ActiveSupport::Inflector.pluralize(hmt_fk)
|
215
|
+
code << " has_many :#{hmt_fk}, through: #{assoc_name.inspect}\n"
|
216
|
+
self.send(:has_many, hmt_fk.to_sym, **{ through: assoc_name })
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end # class definition
|
221
|
+
[built_model, code]
|
222
|
+
end
|
223
|
+
|
224
|
+
|
225
|
+
def build_controller(class_name, model_name, singular_table_name, table_name, relations, matching)
|
226
|
+
code = +"class #{class_name} < ApplicationController\n"
|
227
|
+
built_controller = Class.new(ActionController::Base) do |new_controller_class|
|
228
|
+
Object.const_set(class_name.to_sym, new_controller_class)
|
229
|
+
|
230
|
+
model = model_name.constantize
|
231
|
+
code << " def index\n"
|
232
|
+
code << " @#{table_name} = #{model_name}#{model.primary_key ? ".order(#{model.primary_key.inspect}" : '.all'})\n"
|
233
|
+
code << " end\n"
|
234
|
+
self.define_method :index do
|
235
|
+
relation = model.primary_key ? model.order(model.primary_key) : model.all
|
236
|
+
instance_variable_set("@#{table_name}".to_sym, relation)
|
237
|
+
end
|
238
|
+
|
239
|
+
if model.primary_key
|
240
|
+
code << " def show\n"
|
241
|
+
code << " @#{singular_table_name} = #{model_name}.find(params[:id].split(','))\n"
|
242
|
+
code << " end\n"
|
243
|
+
self.define_method :show do
|
244
|
+
instance_variable_set("@#{singular_table_name}".to_sym, model.find(params[:id].split(',')))
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
# By default, views get marked as read-only
|
249
|
+
unless (relation = relations[matching]).key?(:isView)
|
250
|
+
code << " # (Define :new, :create, :edit, :update, and :destroy)\n"
|
251
|
+
end
|
252
|
+
end # class definition
|
253
|
+
[built_controller, code]
|
254
|
+
end
|
255
|
+
|
256
|
+
def _brick_get_hm_assoc_name(relation, hm_assoc)
|
257
|
+
if relation[:hm_counts][hm_assoc[:assoc_name]] > 1
|
258
|
+
[ActiveSupport::Inflector.pluralize(hm_assoc[:alternate_name]), true]
|
259
|
+
else
|
260
|
+
[ActiveSupport::Inflector.pluralize(hm_assoc[:inverse_table]), nil]
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
# ==========================================================
|
267
|
+
# Get info on all relations during first database connection
|
268
|
+
# ==========================================================
|
269
|
+
|
270
|
+
module ActiveRecord::ConnectionHandling
|
271
|
+
alias old_establish_connection establish_connection
|
272
|
+
def establish_connection(*args)
|
273
|
+
connections = Brick.instance_variable_get(:@relations) ||
|
274
|
+
Brick.instance_variable_set(:@relations, (connections = {}))
|
275
|
+
# puts connections.inspect
|
276
|
+
x = old_establish_connection(*args)
|
277
|
+
# Key our list of relations for this connection off of the connection pool's object_id
|
278
|
+
relations = (connections[ActiveRecord::Base.connection_pool.object_id] ||= Hash.new { |h, k| h[k] = Hash.new { |h, k| h[k] = {} } })
|
279
|
+
|
280
|
+
if relations.empty?
|
281
|
+
schema = 'public'
|
282
|
+
puts ActiveRecord::Base.connection.execute("SELECT current_setting('SEARCH_PATH')").to_a.inspect
|
283
|
+
sql = ActiveRecord::Base.send(:sanitize_sql_array, [
|
284
|
+
"SELECT t.table_name AS relation_name, t.table_type,
|
285
|
+
c.column_name, c.data_type,
|
286
|
+
COALESCE(c.character_maximum_length, c.numeric_precision) AS max_length,
|
287
|
+
tc.constraint_type AS const, kcu.constraint_name AS key
|
288
|
+
FROM INFORMATION_SCHEMA.tables AS t
|
289
|
+
LEFT OUTER JOIN INFORMATION_SCHEMA.columns AS c ON t.table_schema = c.table_schema
|
290
|
+
AND t.table_name = c.table_name
|
291
|
+
LEFT OUTER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu ON
|
292
|
+
-- ON kcu.CONSTRAINT_CATALOG = t.table_catalog AND
|
293
|
+
kcu.CONSTRAINT_SCHEMA = c.table_schema
|
294
|
+
AND kcu.TABLE_NAME = c.table_name
|
295
|
+
AND kcu.position_in_unique_constraint IS NULL
|
296
|
+
AND kcu.ordinal_position = c.ordinal_position
|
297
|
+
LEFT OUTER JOIN INFORMATION_SCHEMA.table_constraints AS tc
|
298
|
+
ON kcu.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA
|
299
|
+
AND kcu.CONSTRAINT_NAME = tc.constraint_name
|
300
|
+
WHERE t.table_schema = ? -- COALESCE(current_setting('SEARCH_PATH'), 'public')
|
301
|
+
-- AND t.table_type IN ('VIEW') -- 'BASE TABLE', 'FOREIGN TABLE'
|
302
|
+
AND t.table_name NOT IN ('pg_stat_statements', 'ar_internal_metadata', 'schema_migrations')
|
303
|
+
ORDER BY 1, t.table_type DESC, c.ordinal_position", schema
|
304
|
+
])
|
305
|
+
ActiveRecord::Base.connection.execute(sql).each do |r|
|
306
|
+
# next if internal_views.include?(r['relation_name']) # Skip internal views such as v_all_assessments
|
307
|
+
|
308
|
+
relation = relations[r['relation_name']]
|
309
|
+
relation[:index] = r['relation_name'].underscore
|
310
|
+
relation[:show] = relation[:index].singularize
|
311
|
+
relation[:index] = relation[:index].pluralize
|
312
|
+
relation[:isView] = true if r['table_type'] == 'VIEW'
|
313
|
+
col_name = r['column_name']
|
314
|
+
cols = relation[:cols] # relation.fetch(:cols) { relation[:cols] = [] }
|
315
|
+
key = case r['const']
|
316
|
+
when 'PRIMARY KEY'
|
317
|
+
relation[:pkey][r['key']] ||= []
|
318
|
+
when 'UNIQUE'
|
319
|
+
relation[:ukeys][r['key']] ||= []
|
320
|
+
# key = (relation[:ukeys] = Hash.new { |h, k| h[k] = [] }) if key.is_a?(Array)
|
321
|
+
# key[r['key']]
|
322
|
+
end
|
323
|
+
key << col_name if key
|
324
|
+
cols[col_name] = [r['data_type'], r['max_length'], r['measures']&.include?(col_name)]
|
325
|
+
# puts "KEY! #{r['relation_name']}.#{col_name} #{r['key']} #{r['const']}" if r['key']
|
326
|
+
end
|
327
|
+
|
328
|
+
sql = ActiveRecord::Base.send(:sanitize_sql_array, [
|
329
|
+
"SELECT kcu1.CONSTRAINT_NAME, kcu1.TABLE_NAME, kcu1.COLUMN_NAME, kcu2.TABLE_NAME
|
330
|
+
FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS rc
|
331
|
+
INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu1
|
332
|
+
ON kcu1.CONSTRAINT_CATALOG = rc.CONSTRAINT_CATALOG
|
333
|
+
AND kcu1.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA
|
334
|
+
AND kcu1.CONSTRAINT_NAME = rc.CONSTRAINT_NAME
|
335
|
+
INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu2
|
336
|
+
ON kcu2.CONSTRAINT_CATALOG = rc.UNIQUE_CONSTRAINT_CATALOG
|
337
|
+
AND kcu2.CONSTRAINT_SCHEMA = rc.UNIQUE_CONSTRAINT_SCHEMA
|
338
|
+
AND kcu2.CONSTRAINT_NAME = rc.UNIQUE_CONSTRAINT_NAME
|
339
|
+
AND kcu2.ORDINAL_POSITION = kcu1.ORDINAL_POSITION
|
340
|
+
WHERE kcu1.CONSTRAINT_SCHEMA = ? -- COALESCE(current_setting('SEARCH_PATH'), 'public')", schema
|
341
|
+
# AND kcu2.TABLE_NAME = ?;", Apartment::Tenant.current, table_name
|
342
|
+
])
|
343
|
+
ActiveRecord::Base.connection.execute(sql).values.each do |fk|
|
344
|
+
bt_assoc_name = fk[2].underscore
|
345
|
+
bt_assoc_name = bt_assoc_name[0..-4] if bt_assoc_name.end_with?('_id')
|
346
|
+
|
347
|
+
bts = (relation = relations[fk[1]]).fetch(:fks) { relation[:fks] = {} }
|
348
|
+
if (assoc_bt = bts[fk[0]])
|
349
|
+
assoc_bt[:fk] = assoc_bt[:fk].is_a?(String) ? [assoc_bt[:fk], fk[2]] : assoc_bt[:fk].concat(fk[2])
|
350
|
+
assoc_bt[:assoc_name] = "#{assoc_bt[:assoc_name]}_#{fk[2]}"
|
351
|
+
else
|
352
|
+
assoc_bt = bts[fk[0]] = { is_bt: true, fk: fk[2], assoc_name: bt_assoc_name, inverse_table: fk[3] }
|
353
|
+
end
|
354
|
+
|
355
|
+
hms = (relation = relations[fk[3]]).fetch(:fks) { relation[:fks] = {} }
|
356
|
+
if (assoc_hm = hms[fk[0]])
|
357
|
+
assoc_hm[:fk] = assoc_hm[:fk].is_a?(String) ? [assoc_hm[:fk], fk[2]] : assoc_hm[:fk].concat(fk[2])
|
358
|
+
assoc_hm[:alternate_name] = "#{assoc_hm[:alternate_name]}_#{bt_assoc_name}" unless assoc_hm[:alternate_name] == bt_assoc_name
|
359
|
+
assoc_hm[:inverse] = assoc_bt
|
360
|
+
else
|
361
|
+
assoc_hm = hms[fk[0]] = { is_bt: false, fk: fk[2], assoc_name: fk[1], alternate_name: bt_assoc_name, inverse_table: fk[1], inverse: assoc_bt }
|
362
|
+
hm_counts = relation.fetch(:hm_counts) { relation[:hm_counts] = {} }
|
363
|
+
hm_counts[fk[1]] = hm_counts.fetch(fk[1]) { 0 } + 1
|
364
|
+
end
|
365
|
+
assoc_bt[:inverse] = assoc_hm
|
366
|
+
# hms[fk[0]] << { is_bt: false, fk: fk[2], assoc_name: fk[1], alternate_name: bt_assoc_name, inverse_table: fk[1] }
|
367
|
+
end
|
368
|
+
# Find associative tables that can be set up for has_many :through
|
369
|
+
relations.each do |_key, tbl|
|
370
|
+
tbl_cols = tbl[:cols].keys
|
371
|
+
fks = tbl[:fks].each_with_object({}) { |fk, s| s[fk.last[:fk]] = fk.last[:inverse_table] if fk.last[:is_bt]; s }
|
372
|
+
# Aside from the primary key and created_at, updated_at,This table has only foreign keys?
|
373
|
+
if fks.length > 1 && (tbl_cols - fks.keys - ['created_at', 'updated_at', 'deleted_at', 'last_update'] - tbl[:pkey].values.first).length.zero?
|
374
|
+
fks.each { |fk| tbl[:hmt_fks][fk.first] = fk.last }
|
375
|
+
end
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
puts "Classes built from tables:"
|
380
|
+
relations.select { |_k, v| !v.key?(:isView) }.keys.each { |k| puts ActiveSupport::Inflector.singularize(k).camelize }
|
381
|
+
puts "Classes built from views:"
|
382
|
+
relations.select { |_k, v| v.key?(:isView) }.keys.each { |k| puts ActiveSupport::Inflector.singularize(k).camelize }
|
383
|
+
# pp relations; nil
|
384
|
+
|
385
|
+
# relations.keys.each { |k| ActiveSupport::Inflector.singularize(k).camelize.constantize }
|
386
|
+
# Layout table describes permissioned hierarchy throughout
|
387
|
+
x
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
# ==========================================
|
392
|
+
|
393
|
+
# :nodoc:
|
394
|
+
module Brick
|
395
|
+
# rubocop:disable Style/CommentedKeyword
|
396
|
+
module Extensions
|
397
|
+
MAX_ID = Arel.sql('MAX(id)')
|
398
|
+
IS_AMOEBA = Gem.loaded_specs['amoeba']
|
399
|
+
|
400
|
+
def self.included(base)
|
401
|
+
base.send :extend, ClassMethods
|
402
|
+
end
|
403
|
+
|
404
|
+
# :nodoc:
|
405
|
+
module ClassMethods
|
406
|
+
|
407
|
+
private
|
408
|
+
|
409
|
+
def _create_class()
|
410
|
+
end
|
411
|
+
end
|
412
|
+
end # module Extensions
|
413
|
+
# rubocop:enable Style/CommentedKeyword
|
414
|
+
|
415
|
+
# Rails < 4.0 doesn't have ActiveRecord::RecordNotUnique, so use the more generic ActiveRecord::ActiveRecordError instead
|
416
|
+
ar_not_unique_error = ActiveRecord.const_defined?('RecordNotUnique') ? ActiveRecord::RecordNotUnique : ActiveRecord::ActiveRecordError
|
417
|
+
class NoUniqueColumnError < ar_not_unique_error
|
418
|
+
end
|
419
|
+
|
420
|
+
# Rails < 4.2 doesn't have ActiveRecord::RecordInvalid, so use the more generic ActiveRecord::ActiveRecordError instead
|
421
|
+
ar_invalid_error = ActiveRecord.const_defined?('RecordInvalid') ? ActiveRecord::RecordInvalid : ActiveRecord::ActiveRecordError
|
422
|
+
class LessThanHalfAreMatchingColumnsError < ar_invalid_error
|
423
|
+
end
|
424
|
+
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
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Brick
|
4
|
+
module Rails
|
5
|
+
# See http://guides.rubyonrails.org/engines.html
|
6
|
+
class Engine < ::Rails::Engine
|
7
|
+
# paths['app/models'] << 'lib/brick/frameworks/active_record/models'
|
8
|
+
config.brick = ActiveSupport::OrderedOptions.new
|
9
|
+
initializer 'brick.initialisation' do |app|
|
10
|
+
# Auto-routing behaviour
|
11
|
+
if (::Brick.enable_routes = app.config.brick.fetch(:enable_routes, true))
|
12
|
+
::Brick.append_routes
|
13
|
+
end
|
14
|
+
# Brick.enable_models = app.config.brick.fetch(:enable_models, true)
|
15
|
+
# Brick.enable_controllers = app.config.brick.fetch(:enable_controllers, true)
|
16
|
+
# Brick.enable_views = app.config.brick.fetch(:enable_views, true)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rspec/core'
|
4
|
+
require 'rspec/matchers'
|
5
|
+
# require 'brick/frameworks/rspec/helpers'
|
6
|
+
|
7
|
+
RSpec.configure do |config|
|
8
|
+
# config.include ::Brick::RSpec::Helpers::InstanceMethods
|
9
|
+
# config.extend ::Brick::RSpec::Helpers::ClassMethods
|
10
|
+
|
11
|
+
# config.before(:each) do
|
12
|
+
# ::Brick.enable_routes = false
|
13
|
+
# end
|
14
|
+
|
15
|
+
# config.before(:each, df_importing: true) do
|
16
|
+
# ::Brick.enable_routes = true
|
17
|
+
# end
|
18
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Brick
|
4
|
+
module Serializers
|
5
|
+
# An alternate serializer for, e.g. `versions.object`.
|
6
|
+
module JSON
|
7
|
+
extend self # makes all instance methods become module methods as well
|
8
|
+
|
9
|
+
def load(string)
|
10
|
+
ActiveSupport::JSON.decode string
|
11
|
+
end
|
12
|
+
|
13
|
+
def dump(object)
|
14
|
+
ActiveSupport::JSON.encode object
|
15
|
+
end
|
16
|
+
|
17
|
+
# Returns a SQL LIKE condition to be used to match the given field and
|
18
|
+
# value in the serialized object.
|
19
|
+
def where_object_condition(arel_field, field, value)
|
20
|
+
# Convert to JSON to handle strings and nulls correctly.
|
21
|
+
json_value = value.to_json
|
22
|
+
|
23
|
+
# If the value is a number, we need to ensure that we find the next
|
24
|
+
# character too, which is either `,` or `}`, to ensure that searching
|
25
|
+
# for the value 12 doesn't yield false positives when the value is
|
26
|
+
# 123.
|
27
|
+
if value.is_a? Numeric
|
28
|
+
arel_field.matches("%\"#{field}\":#{json_value},%")
|
29
|
+
.or(arel_field.matches("%\"#{field}\":#{json_value}}%"))
|
30
|
+
else
|
31
|
+
arel_field.matches("%\"#{field}\":#{json_value}%")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
module Brick
|
6
|
+
module Serializers
|
7
|
+
# The default serializer for, e.g. `versions.object`.
|
8
|
+
module YAML
|
9
|
+
extend self # makes all instance methods become module methods as well
|
10
|
+
|
11
|
+
def load(string)
|
12
|
+
::YAML.safe_load string
|
13
|
+
end
|
14
|
+
|
15
|
+
def dump(object)
|
16
|
+
::YAML.dump object
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns a SQL LIKE condition to be used to match the given field and
|
20
|
+
# value in the serialized object.
|
21
|
+
def where_object_condition(arel_field, field, value)
|
22
|
+
arel_field.matches("%\n#{field}: #{value}\n%")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|