brick 0.1.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|