hyper-model 1.0.alpha1.2 → 1.0.alpha1.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +4 -1
- data/.rspec +0 -1
- data/Gemfile +6 -5
- data/Rakefile +27 -3
- data/hyper-model.gemspec +11 -19
- data/lib/active_record_base.rb +105 -33
- data/lib/enumerable/pluck.rb +3 -2
- data/lib/hyper-model.rb +4 -1
- data/lib/hyper_model/version.rb +1 -1
- data/lib/hyper_react/input_tags.rb +2 -1
- data/lib/reactive_record/active_record/associations.rb +130 -34
- data/lib/reactive_record/active_record/base.rb +32 -0
- data/lib/reactive_record/active_record/class_methods.rb +124 -52
- data/lib/reactive_record/active_record/error.rb +2 -0
- data/lib/reactive_record/active_record/errors.rb +8 -4
- data/lib/reactive_record/active_record/instance_methods.rb +73 -5
- data/lib/reactive_record/active_record/public_columns_hash.rb +25 -26
- data/lib/reactive_record/active_record/reactive_record/backing_record_inspector.rb +22 -5
- data/lib/reactive_record/active_record/reactive_record/base.rb +50 -24
- data/lib/reactive_record/active_record/reactive_record/collection.rb +226 -68
- data/lib/reactive_record/active_record/reactive_record/dummy_polymorph.rb +22 -0
- data/lib/reactive_record/active_record/reactive_record/dummy_value.rb +27 -15
- data/lib/reactive_record/active_record/reactive_record/getters.rb +33 -10
- data/lib/reactive_record/active_record/reactive_record/isomorphic_base.rb +81 -51
- data/lib/reactive_record/active_record/reactive_record/lookup_tables.rb +5 -5
- data/lib/reactive_record/active_record/reactive_record/operations.rb +10 -3
- data/lib/reactive_record/active_record/reactive_record/scoped_collection.rb +3 -0
- data/lib/reactive_record/active_record/reactive_record/setters.rb +105 -68
- data/lib/reactive_record/active_record/reactive_record/while_loading.rb +249 -32
- data/lib/reactive_record/broadcast.rb +62 -25
- data/lib/reactive_record/interval.rb +3 -3
- data/lib/reactive_record/permissions.rb +14 -2
- data/lib/reactive_record/scope_description.rb +3 -2
- data/lib/reactive_record/server_data_cache.rb +99 -49
- data/polymorph-notes.md +143 -0
- data/spec_fails.txt +3 -0
- metadata +54 -153
- data/Gemfile.lock +0 -421
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d85eab56099328a86bb19e2911f16bd1b5b9a6e98a79d7c912898dce7775e876
|
4
|
+
data.tar.gz: cf0f2c03f22c84710e76e5177f8f54f8847c9e1e671f3c5aab38b8c77abbfc8f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8095f00d457423d733f0d1f8ea0118a47b5084b282bc224b9a3a7b90e8ea9321487e849932b48a4f2fe0d75ee1047033150da036019cdc7be0556d7fe3d71eb2
|
7
|
+
data.tar.gz: b734954d84ae3b363a9fceff250b0a49a4062ec5d893a96197d4c9cd9f6ccb3adbdd90d8f41f325a51faffaef08c7b976217005fdd3b85c98e9fd219575dd1ec
|
data/.gitignore
CHANGED
@@ -11,7 +11,6 @@ spec/test_app/tmp/
|
|
11
11
|
spec/test_app/db/test.sqlite3
|
12
12
|
spec/test_app/log/test.log
|
13
13
|
spec/test_app/log/development.log
|
14
|
-
spec/test_app/Gemfile.lock
|
15
14
|
/synchromesh-simple-poller-store
|
16
15
|
/synchromesh-pusher-channel-store
|
17
16
|
/examples/action-cable/rails_cache_dir/
|
@@ -34,3 +33,7 @@ public/assets/*
|
|
34
33
|
# ingore Idea
|
35
34
|
.idea
|
36
35
|
.vscode
|
36
|
+
|
37
|
+
# ignore Gemfile.locks https://yehudakatz.com/2010/12/16/clarifying-the-roles-of-the-gemspec-and-gemfile/
|
38
|
+
/spec/test_app/Gemfile.lock
|
39
|
+
/Gemfile.lock
|
data/.rspec
CHANGED
data/Gemfile
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
source 'https://rubygems.org'
|
2
|
-
#gem "opal-jquery", git: "https://github.com/opal/opal-jquery.git", branch: "master"
|
3
|
-
# hyper-model is still using an ancient inlined version of hyper-spec
|
4
|
-
#gem 'hyper-spec', path: '../hyper-spec'
|
5
|
-
gem 'hyperstack-config', path: '../hyperstack-config'
|
6
2
|
gem 'hyper-state', path: '../hyper-state'
|
7
3
|
gem 'hyper-component', path: '../hyper-component'
|
8
4
|
gem 'hyper-operation', path: '../hyper-operation'
|
9
|
-
|
5
|
+
gem 'hyper-spec', path: '../hyper-spec'
|
6
|
+
gem 'hyper-trace', path: '../hyper-trace'
|
7
|
+
gem 'hyperstack-config', path: '../hyperstack-config'
|
8
|
+
unless ENV['OPAL_VERSION']&.match("0.11")
|
9
|
+
gem 'opal-browser', git: 'https://github.com/opal/opal-browser'
|
10
|
+
end
|
10
11
|
gemspec
|
data/Rakefile
CHANGED
@@ -1,17 +1,41 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
2
|
require "rspec/core/rake_task"
|
3
3
|
|
4
|
+
def run_batches(batches)
|
5
|
+
failed = false
|
6
|
+
batches.each do |batch|
|
7
|
+
begin
|
8
|
+
Rake::Task["spec:batch#{batch}"].invoke
|
9
|
+
rescue SystemExit
|
10
|
+
failed = true
|
11
|
+
end
|
12
|
+
end
|
13
|
+
exit 1 if failed
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
task :part1 do
|
18
|
+
run_batches(1..2)
|
19
|
+
end
|
20
|
+
|
21
|
+
task :part2 do
|
22
|
+
run_batches(3..4)
|
23
|
+
end
|
24
|
+
|
25
|
+
task :part3 do
|
26
|
+
run_batches(5..7)
|
27
|
+
end
|
28
|
+
|
4
29
|
task :spec do
|
5
|
-
(1..7)
|
30
|
+
run_batches(1..7)
|
6
31
|
end
|
7
32
|
|
8
33
|
namespace :spec do
|
9
34
|
task :prepare do
|
10
|
-
sh %(cd spec/test_app; bundle exec rails db:setup)
|
35
|
+
sh %(cd spec/test_app; rm db/schema.rb; RAILS_ENV=test bundle exec rails db:setup; RAILS_ENV=test bundle exec rails db:migrate)
|
11
36
|
end
|
12
37
|
(1..7).each do |batch|
|
13
38
|
RSpec::Core::RakeTask.new(:"batch#{batch}") do |t|
|
14
|
-
t.fail_on_error = false unless batch == 7
|
15
39
|
t.pattern = "spec/batch#{batch}/**/*_spec.rb"
|
16
40
|
end
|
17
41
|
end
|
data/hyper-model.gemspec
CHANGED
@@ -27,31 +27,24 @@ Gem::Specification.new do |spec|
|
|
27
27
|
|
28
28
|
spec.add_dependency 'activemodel'
|
29
29
|
spec.add_dependency 'activerecord', '>= 4.0.0'
|
30
|
-
spec.add_dependency 'hyper-component', HyperModel::VERSION
|
31
30
|
spec.add_dependency 'hyper-operation', HyperModel::VERSION
|
31
|
+
|
32
32
|
spec.add_development_dependency 'bundler'
|
33
|
-
spec.add_development_dependency 'capybara'
|
34
|
-
spec.add_development_dependency 'chromedriver-helper', '1.2.0'
|
35
|
-
spec.add_development_dependency 'libv8', '~> 6.3.0' # see https://github.com/discourse/mini_racer/issues/92
|
36
|
-
spec.add_development_dependency 'mini_racer', '~> 0.1.15'
|
37
|
-
spec.add_development_dependency 'selenium-webdriver'
|
38
33
|
spec.add_development_dependency 'database_cleaner'
|
39
34
|
spec.add_development_dependency 'factory_bot_rails'
|
40
|
-
|
41
|
-
spec.add_development_dependency '
|
42
|
-
spec.add_development_dependency '
|
43
|
-
spec.add_development_dependency '
|
44
|
-
spec.add_development_dependency 'opal-rails', '
|
45
|
-
spec.add_development_dependency 'parser'
|
46
|
-
spec.add_development_dependency 'pry'
|
35
|
+
spec.add_development_dependency 'hyper-spec', HyperModel::VERSION
|
36
|
+
spec.add_development_dependency 'hyper-trace', HyperModel::VERSION
|
37
|
+
spec.add_development_dependency 'mini_racer'
|
38
|
+
spec.add_development_dependency 'pg'
|
39
|
+
spec.add_development_dependency 'opal-rails', '>= 0.9.4', '< 2.0'
|
47
40
|
spec.add_development_dependency 'pry-rescue'
|
41
|
+
spec.add_development_dependency 'pry-stack_explorer'
|
48
42
|
spec.add_development_dependency 'puma'
|
49
43
|
spec.add_development_dependency 'pusher'
|
50
44
|
spec.add_development_dependency 'pusher-fake'
|
51
|
-
spec.add_development_dependency 'rails', '>=
|
45
|
+
spec.add_development_dependency 'rails', ENV['RAILS_VERSION'] || '>= 5.0.0', '< 7.0'
|
52
46
|
spec.add_development_dependency 'rake'
|
53
47
|
spec.add_development_dependency 'react-rails', '>= 2.4.0', '< 2.5.0'
|
54
|
-
spec.add_development_dependency 'reactrb-rails-generator'
|
55
48
|
spec.add_development_dependency 'rspec-collection_matchers'
|
56
49
|
spec.add_development_dependency 'rspec-expectations'
|
57
50
|
spec.add_development_dependency 'rspec-its'
|
@@ -59,11 +52,10 @@ Gem::Specification.new do |spec|
|
|
59
52
|
spec.add_development_dependency 'rspec-rails'
|
60
53
|
spec.add_development_dependency 'rspec-steps', '~> 2.1.1'
|
61
54
|
spec.add_development_dependency 'rspec-wait'
|
62
|
-
spec.add_development_dependency 'rubocop'
|
55
|
+
spec.add_development_dependency 'rubocop' #, '~> 0.51.0'
|
63
56
|
spec.add_development_dependency 'shoulda'
|
64
57
|
spec.add_development_dependency 'shoulda-matchers'
|
65
|
-
spec.add_development_dependency 'spring-commands-rspec'
|
66
|
-
spec.add_development_dependency 'sqlite3'
|
58
|
+
spec.add_development_dependency 'spring-commands-rspec', '~> 1.0.4'
|
59
|
+
spec.add_development_dependency 'sqlite3', '~> 1.4.2' # see https://github.com/rails/rails/issues/35153, '~> 1.3.6'
|
67
60
|
spec.add_development_dependency 'timecop', '~> 0.8.1'
|
68
|
-
spec.add_development_dependency 'unparser'
|
69
61
|
end
|
data/lib/active_record_base.rb
CHANGED
@@ -5,17 +5,46 @@ module ActiveRecord
|
|
5
5
|
# processes these arguments, and the will always leave the true server side scoping
|
6
6
|
# proc in the `:server` opts. This method is common to client and server.
|
7
7
|
class Base
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
args[0]
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
8
|
+
class << self
|
9
|
+
def _synchromesh_scope_args_check(args)
|
10
|
+
opts = if args.count == 2 && args[1].is_a?(Hash)
|
11
|
+
args[1].merge(server: args[0])
|
12
|
+
elsif args[0].is_a? Hash
|
13
|
+
args[0]
|
14
|
+
else
|
15
|
+
{ server: args[0] }
|
16
|
+
end
|
17
|
+
return opts if opts[:server].respond_to?(:call) || RUBY_ENGINE == 'opal'
|
18
|
+
raise 'must provide either a proc as the first arg or by the '\
|
19
|
+
'`:server` option to scope and default_scope methods'
|
20
|
+
end
|
21
|
+
|
22
|
+
alias pre_hyperstack_has_and_belongs_to_many has_and_belongs_to_many unless RUBY_ENGINE == 'opal'
|
23
|
+
|
24
|
+
def has_and_belongs_to_many(other, opts = {}, &block)
|
25
|
+
join_table_name = [other.to_s, table_name].sort.join('_')
|
26
|
+
join_model_name = "HyperstackInternalHabtm#{join_table_name.singularize.camelize}"
|
27
|
+
join_model =
|
28
|
+
if Object.const_defined? join_model_name
|
29
|
+
Object.const_get(join_model_name)
|
30
|
+
else
|
31
|
+
Object.const_set(join_model_name, Class.new(ActiveRecord::Base))
|
32
|
+
end
|
33
|
+
|
34
|
+
join_model.class_eval { belongs_to other.to_s.singularize.to_sym }
|
35
|
+
|
36
|
+
has_many join_model_name.underscore.pluralize.to_sym
|
37
|
+
|
38
|
+
if RUBY_ENGINE == 'opal'
|
39
|
+
Object.const_set("HABTM_#{other.to_s.camelize}", join_model)
|
40
|
+
join_model.inheritance_column = nil
|
41
|
+
has_many other, through: join_model_name.underscore.pluralize.to_sym
|
42
|
+
else
|
43
|
+
join_model.table_name = join_table_name
|
44
|
+
join_model.belongs_to other
|
45
|
+
pre_hyperstack_has_and_belongs_to_many(other, opts, &block)
|
46
|
+
end
|
47
|
+
end
|
19
48
|
end
|
20
49
|
end
|
21
50
|
if RUBY_ENGINE != 'opal'
|
@@ -29,7 +58,7 @@ module ActiveRecord
|
|
29
58
|
class ReactiveRecordPsuedoRelationArray < Array
|
30
59
|
attr_accessor :__synchromesh_permission_granted
|
31
60
|
attr_accessor :acting_user
|
32
|
-
def __secure_collection_check(
|
61
|
+
def __secure_collection_check(*)
|
33
62
|
self
|
34
63
|
end
|
35
64
|
end
|
@@ -39,11 +68,13 @@ module ActiveRecord
|
|
39
68
|
class Relation
|
40
69
|
attr_accessor :__synchromesh_permission_granted
|
41
70
|
attr_accessor :acting_user
|
42
|
-
|
71
|
+
|
72
|
+
def __secure_collection_check(cache_item)
|
43
73
|
return self if __synchromesh_permission_granted
|
44
|
-
return self if __secure_remote_access_to_all(self, acting_user).__synchromesh_permission_granted
|
45
|
-
return self if __secure_remote_access_to_unscoped(self, acting_user).__synchromesh_permission_granted
|
46
|
-
Hyperstack::InternalPolicy.raise_operation_access_violation(
|
74
|
+
return self if __secure_remote_access_to_all(self, cache_item.acting_user).__synchromesh_permission_granted
|
75
|
+
return self if __secure_remote_access_to_unscoped(self, cache_item.acting_user).__synchromesh_permission_granted
|
76
|
+
Hyperstack::InternalPolicy.raise_operation_access_violation(
|
77
|
+
:scoped_permission_not_granted, "Access denied for #{cache_item}")
|
47
78
|
end
|
48
79
|
end
|
49
80
|
# Monkey patches and extensions to base
|
@@ -85,11 +116,14 @@ module ActiveRecord
|
|
85
116
|
this.acting_user = acting_user
|
86
117
|
# returns a PsuedoRelationArray which will respond to the
|
87
118
|
# __secure_collection_check method
|
88
|
-
ReactiveRecordPsuedoRelationArray.new([this.instance_exec(*args, &block)])
|
119
|
+
ReactiveRecordPsuedoRelationArray.new([*this.instance_exec(*args, &block)])
|
89
120
|
ensure
|
90
121
|
this.acting_user = old
|
91
122
|
end
|
92
123
|
end
|
124
|
+
singleton_class.send(:define_method, "_#{name}") do |*args|
|
125
|
+
all.instance_exec(*args, &block)
|
126
|
+
end
|
93
127
|
singleton_class.send(:define_method, name) do |*args|
|
94
128
|
all.instance_exec(*args, &block)
|
95
129
|
end
|
@@ -250,21 +284,7 @@ module ActiveRecord
|
|
250
284
|
pre_syncromesh_has_many name, *args, opts.except(:regulate), &block
|
251
285
|
end
|
252
286
|
|
253
|
-
|
254
|
-
# No explicit security checks are needed here, as the data returned by these objects
|
255
|
-
# will be further processedand checked before returning. I.e. it is not possible to
|
256
|
-
# simply return `find(1)` but if you try returning `find(1).name` the permission system
|
257
|
-
# will check to see if the name attribute can be legally sent to the current acting user.
|
258
|
-
|
259
|
-
def __secure_remote_access_to_find(_self, _acting_user, *args)
|
260
|
-
find(*args)
|
261
|
-
end
|
262
|
-
|
263
|
-
def __secure_remote_access_to_find_by(_self, _acting_user, *args)
|
264
|
-
find_by(*args)
|
265
|
-
end
|
266
|
-
|
267
|
-
%i[belongs_to has_one].each do |macro|
|
287
|
+
%i[belongs_to has_one composed_of].each do |macro|
|
268
288
|
alias_method :"pre_syncromesh_#{macro}", macro
|
269
289
|
define_method(macro) do |name, *aargs, &block|
|
270
290
|
define_method(:"__secure_remote_access_to_#{name}") do |this, _acting_user, *args|
|
@@ -279,6 +299,12 @@ module ActiveRecord
|
|
279
299
|
Hyperstack::InternalPolicy.raise_operation_access_violation(:scoped_denied, "#{self.class} regulation denies scope access. Called from #{caller_locations(1)}")
|
280
300
|
end
|
281
301
|
|
302
|
+
unless method_defined? :saved_changes # for backwards compatibility to Rails < 5.1.7
|
303
|
+
def saved_changes
|
304
|
+
previous_changes
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
282
308
|
# call do_not_synchronize to block synchronization of a model
|
283
309
|
|
284
310
|
def self.do_not_synchronize
|
@@ -295,17 +321,28 @@ module ActiveRecord
|
|
295
321
|
self.class.do_not_synchronize?
|
296
322
|
end
|
297
323
|
|
324
|
+
before_create :synchromesh_mark_update_time
|
325
|
+
before_update :synchromesh_mark_update_time
|
326
|
+
before_destroy :synchromesh_mark_update_time
|
327
|
+
|
328
|
+
attr_reader :__synchromesh_update_time
|
329
|
+
|
330
|
+
def synchromesh_mark_update_time
|
331
|
+
@__synchromesh_update_time = Time.now.to_f
|
332
|
+
end
|
333
|
+
|
298
334
|
after_commit :synchromesh_after_create, on: [:create]
|
299
335
|
after_commit :synchromesh_after_change, on: [:update]
|
300
336
|
after_commit :synchromesh_after_destroy, on: [:destroy]
|
301
337
|
|
302
338
|
def synchromesh_after_create
|
339
|
+
puts "#{self}.synchromesh_after_create: #{do_not_synchronize?} channels: #{Hyperstack::Connection.active}" if Hyperstack::Connection.show_diagnostics
|
303
340
|
return if do_not_synchronize?
|
304
341
|
ReactiveRecord::Broadcast.after_commit :create, self
|
305
342
|
end
|
306
343
|
|
307
344
|
def synchromesh_after_change
|
308
|
-
return if do_not_synchronize? ||
|
345
|
+
return if do_not_synchronize? || saved_changes.empty?
|
309
346
|
ReactiveRecord::Broadcast.after_commit :change, self
|
310
347
|
end
|
311
348
|
|
@@ -324,6 +361,41 @@ module ActiveRecord
|
|
324
361
|
%i[limit offset].each do |scope|
|
325
362
|
regulate_scope(scope) {}
|
326
363
|
end
|
364
|
+
|
365
|
+
finder_method :__hyperstack_internal_scoped_last do
|
366
|
+
last
|
367
|
+
end
|
368
|
+
|
369
|
+
scope :__hyperstack_internal_scoped_last_n, ->(n) { last(n) }
|
370
|
+
|
371
|
+
# implements find_by inside of scopes. For security reasons we return nil
|
372
|
+
# if we cannot view at least the id of found record. Otherwise a hacker
|
373
|
+
# could tell if a record exists depending on whether an access violation
|
374
|
+
# (i.e. it exists) or nil (it doesn't exist is returned.) Note that
|
375
|
+
# view of id is permitted as long as any attribute of the record is
|
376
|
+
# accessible.
|
377
|
+
finder_method :__hyperstack_internal_scoped_find_by do |attrs|
|
378
|
+
begin
|
379
|
+
found = find_by(attrs)
|
380
|
+
found && found.check_permission_with_acting_user(acting_user, :view_permitted?, :id)
|
381
|
+
rescue Hyperstack::AccessViolation => e
|
382
|
+
message = []
|
383
|
+
message << Pastel.new.red("\n\nHYPERSTACK Access violation during find_by operation.")
|
384
|
+
message << Pastel.new.red("Access to the found record's id is not permitted. nil will be returned")
|
385
|
+
message << " #{self.name}.find_by("
|
386
|
+
message << attrs.collect do |attr, value|
|
387
|
+
" #{attr}: '#{value.inspect.truncate(120, separator: '...')}'"
|
388
|
+
end.join(",\n")
|
389
|
+
message << " )"
|
390
|
+
message << "\n#{e.details}\n"
|
391
|
+
Hyperstack.on_error('find_by', self, attrs, message.join("\n"))
|
392
|
+
nil
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
scope :__hyperstack_internal_where_hash_scope, ->(*args) { where(*args) }
|
397
|
+
|
398
|
+
scope :__hyperstack_internal_where_sql_scope, ->(*args) { where(*args) }
|
327
399
|
end
|
328
400
|
end
|
329
401
|
|
data/lib/enumerable/pluck.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# Add pluck to enumerable... its already done for us in rails 5+
|
2
2
|
module Enumerable
|
3
|
-
def pluck(
|
4
|
-
map { |element| element[key] }
|
3
|
+
def pluck(*keys)
|
4
|
+
map { |element| keys.map { |key| element[key] } }
|
5
|
+
.flatten(keys.count > 1 ? 0 : 1)
|
5
6
|
end
|
6
7
|
end unless Enumerable.method_defined? :pluck
|
data/lib/hyper-model.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
require 'set'
|
2
2
|
require 'hyperstack-config'
|
3
|
-
|
3
|
+
|
4
|
+
Hyperstack.import 'hyper-model'
|
5
|
+
|
4
6
|
if RUBY_ENGINE == 'opal'
|
5
7
|
require 'hyper-operation'
|
6
8
|
require 'active_support'
|
@@ -20,6 +22,7 @@ if RUBY_ENGINE == 'opal'
|
|
20
22
|
require "reactive_record/active_record/reactive_record/isomorphic_base"
|
21
23
|
require 'reactive_record/active_record/reactive_record/dummy_value'
|
22
24
|
require 'reactive_record/active_record/reactive_record/column_types'
|
25
|
+
require 'reactive_record/active_record/reactive_record/dummy_polymorph'
|
23
26
|
require "reactive_record/active_record/aggregations"
|
24
27
|
require "reactive_record/active_record/associations"
|
25
28
|
require "reactive_record/active_record/reactive_record/backing_record_inspector"
|
data/lib/hyper_model/version.rb
CHANGED
@@ -3,7 +3,7 @@ module ActiveRecord
|
|
3
3
|
class Base
|
4
4
|
|
5
5
|
def self.reflect_on_all_associations
|
6
|
-
|
6
|
+
@associations ||= superclass.instance_eval { @associations&.dup || [] }
|
7
7
|
end
|
8
8
|
|
9
9
|
def self.reflect_on_association(attr)
|
@@ -11,7 +11,7 @@ module ActiveRecord
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def self.reflect_on_association_by_foreign_key(key)
|
14
|
-
reflection_finder { |assoc| assoc.association_foreign_key == key }
|
14
|
+
reflection_finder { |assoc| assoc.association_foreign_key == key && assoc.macro != :has_many }
|
15
15
|
end
|
16
16
|
|
17
17
|
def self.reflection_finder(&block)
|
@@ -32,22 +32,55 @@ module ActiveRecord
|
|
32
32
|
module Associations
|
33
33
|
|
34
34
|
class AssociationReflection
|
35
|
-
|
35
|
+
attr_reader :klass_name
|
36
36
|
attr_reader :association_foreign_key
|
37
37
|
attr_reader :attribute
|
38
38
|
attr_reader :macro
|
39
39
|
attr_reader :owner_class
|
40
40
|
attr_reader :source
|
41
|
+
attr_reader :source_type
|
42
|
+
attr_reader :options
|
43
|
+
attr_reader :polymorphic_type_attribute
|
41
44
|
|
42
45
|
def initialize(owner_class, macro, name, options = {})
|
43
46
|
owner_class.reflect_on_all_associations << self
|
44
47
|
@owner_class = owner_class
|
45
48
|
@macro = macro
|
46
49
|
@options = options
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
+
unless options[:polymorphic]
|
51
|
+
@klass_name = options[:class_name] || (collection? && name.camelize.singularize) || name.camelize
|
52
|
+
end
|
53
|
+
|
54
|
+
if @klass_name < ActiveRecord::Base
|
55
|
+
@klass = @klass_name
|
56
|
+
@klass_name = @klass_name.name
|
57
|
+
end rescue nil
|
58
|
+
|
59
|
+
@association_foreign_key =
|
60
|
+
options[:foreign_key] ||
|
61
|
+
(macro == :belongs_to && "#{name}_id") ||
|
62
|
+
(options[:as] && "#{options[:as]}_id") ||
|
63
|
+
(options[:polymorphic] && "#{name}_id") ||
|
64
|
+
"#{@owner_class.name.underscore}_id"
|
65
|
+
if options[:through]
|
66
|
+
@source = options[:source] || @klass_name.underscore
|
67
|
+
@source_type = options[:source_type] || @klass_name
|
68
|
+
end
|
69
|
+
@polymorphic_type_attribute = "#{name}_type" if options[:polymorphic]
|
50
70
|
@attribute = name
|
71
|
+
@through_associations = Hash.new { |_h, k| [] unless k }
|
72
|
+
end
|
73
|
+
|
74
|
+
def collection?
|
75
|
+
@macro == :has_many
|
76
|
+
end
|
77
|
+
|
78
|
+
def singular?
|
79
|
+
@macro != :has_many
|
80
|
+
end
|
81
|
+
|
82
|
+
def habtm?
|
83
|
+
through_association&.klass_name =~ /^HyperstackInternalHabtm/
|
51
84
|
end
|
52
85
|
|
53
86
|
def through_association
|
@@ -62,57 +95,120 @@ module ActiveRecord
|
|
62
95
|
|
63
96
|
alias through_association? through_association
|
64
97
|
|
65
|
-
|
98
|
+
# class Membership < ActiveRecord::Base
|
99
|
+
# belongs_to :uzer
|
100
|
+
# belongs_to :memerable, polymorphic: true
|
101
|
+
# end
|
102
|
+
#
|
103
|
+
# class Project < ActiveRecord::Base
|
104
|
+
# has_many :memberships, as: :memerable, dependent: :destroy
|
105
|
+
# has_many :uzers, through: :memberships
|
106
|
+
# end
|
107
|
+
#
|
108
|
+
# class Group < ActiveRecord::Base
|
109
|
+
# has_many :memberships, as: :memerable, dependent: :destroy
|
110
|
+
# has_many :uzers, through: :memberships
|
111
|
+
# end
|
112
|
+
#
|
113
|
+
# class Uzer < ActiveRecord::Base
|
114
|
+
# has_many :memberships
|
115
|
+
# has_many :groups, through: :memberships, source: :memerable, source_type: 'Group'
|
116
|
+
# has_many :projects, through: :memberships, source: :memerable, source_type: 'Project'
|
117
|
+
# end
|
118
|
+
|
119
|
+
# so find the belongs_to relationship whose attribute == ta.source
|
120
|
+
# now find the inverse of that relationship using source_value as the model
|
121
|
+
# now find any has many through relationships that use that relationship as there source.
|
122
|
+
# each of those attributes in the source_value have to be updated.
|
123
|
+
|
124
|
+
# self is the through association
|
125
|
+
|
126
|
+
|
127
|
+
def through_associations(model)
|
128
|
+
# given self is a belongs_to association currently pointing to model
|
66
129
|
# find all associations that use the inverse association as the through association
|
67
130
|
# that is find all associations that are using this association in a through relationship
|
68
|
-
|
69
|
-
|
131
|
+
the_klass = klass(model)
|
132
|
+
@through_associations[the_klass] ||= the_klass.reflect_on_all_associations.select do |assoc|
|
133
|
+
assoc.through_association&.inverse == self
|
70
134
|
end
|
71
135
|
end
|
72
136
|
|
73
|
-
def
|
74
|
-
#
|
75
|
-
#
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
assoc.source == attribute
|
137
|
+
def source_belongs_to_association # private
|
138
|
+
# given self is a has_many_through association return the corresponding belongs_to association
|
139
|
+
# for the source
|
140
|
+
@source_belongs_to_association ||=
|
141
|
+
through_association.inverse.owner_class.reflect_on_all_associations.detect do |sibling|
|
142
|
+
sibling.attribute == source
|
80
143
|
end
|
81
|
-
end.flatten
|
82
144
|
end
|
83
145
|
|
84
|
-
def
|
85
|
-
|
86
|
-
|
146
|
+
def source_associations(model)
|
147
|
+
# given self is a has_many_through association find the source_association for the given model
|
148
|
+
source_belongs_to_association.through_associations(model)
|
87
149
|
end
|
88
150
|
|
89
|
-
|
90
|
-
|
151
|
+
alias :polymorphic? polymorphic_type_attribute
|
152
|
+
|
153
|
+
def inverse(model = nil)
|
154
|
+
return @inverse if @inverse
|
155
|
+
ta = through_association
|
156
|
+
found = ta ? ta.inverse : find_inverse(model)
|
157
|
+
@inverse = found unless polymorphic?
|
158
|
+
found
|
159
|
+
end
|
160
|
+
|
161
|
+
def inverse_of(model = nil)
|
162
|
+
inverse(model).attribute
|
91
163
|
end
|
92
164
|
|
93
|
-
def find_inverse
|
94
|
-
|
165
|
+
def find_inverse(model) # private
|
166
|
+
the_klass = klass(model)
|
167
|
+
the_klass.reflect_on_all_associations.each do |association|
|
168
|
+
next if association == self
|
95
169
|
next if association.association_foreign_key != @association_foreign_key
|
96
|
-
next if association.klass != @owner_class
|
97
170
|
next if association.attribute == attribute
|
98
|
-
return association if klass ==
|
171
|
+
return association if association.polymorphic? || association.klass == owner_class
|
172
|
+
end
|
173
|
+
raise "could not find inverse of polymorphic belongs_to: #{model.inspect} #{self.inspect}" if options[:polymorphic]
|
174
|
+
# instead of raising an error go ahead and create the inverse relationship if it does not exist.
|
175
|
+
# https://github.com/hyperstack-org/hyperstack/issues/89
|
176
|
+
if macro == :belongs_to
|
177
|
+
Hyperstack::Component::IsomorphicHelpers.log "**** warning dynamically adding relationship: #{the_klass}.has_many :#{@owner_class.name.underscore.pluralize}, foreign_key: #{@association_foreign_key}", :warning
|
178
|
+
the_klass.has_many @owner_class.name.underscore.pluralize, foreign_key: @association_foreign_key
|
179
|
+
elsif options[:as]
|
180
|
+
Hyperstack::Component::IsomorphicHelpers.log "**** warning dynamically adding relationship: #{the_klass}.belongs_to :#{options[:as]}, polymorphic: true", :warning
|
181
|
+
the_klass.belongs_to options[:as], polymorphic: true
|
182
|
+
else
|
183
|
+
Hyperstack::Component::IsomorphicHelpers.log "**** warning dynamically adding relationship: #{the_klass}.belongs_to :#{@owner_class.name.underscore}, foreign_key: #{@association_foreign_key}", :warning
|
184
|
+
the_klass.belongs_to @owner_class.name.underscore, foreign_key: @association_foreign_key
|
99
185
|
end
|
100
|
-
raise "Association #{@owner_class}.#{attribute} "\
|
101
|
-
"(foreign_key: #{@association_foreign_key}) "\
|
102
|
-
"has no inverse in #{@klass_name}"
|
103
186
|
end
|
104
187
|
|
105
|
-
def klass
|
106
|
-
@klass ||= Object.const_get(@klass_name)
|
188
|
+
def klass(model = nil)
|
189
|
+
@klass ||= Object.const_get(@klass_name) if @klass_name
|
190
|
+
if @klass && model && !(model.class <= @klass || @klass <= model.class)
|
191
|
+
# TODO: added || @klass <= model.class can both cases really happen I guess so
|
192
|
+
raise "internal error: provided model #{model} is not subclass of #{@klass}"
|
193
|
+
end
|
194
|
+
raise 'no model supplied for polymorphic relationship' unless @klass || model
|
195
|
+
@klass || model.class
|
107
196
|
end
|
108
197
|
|
109
198
|
def collection?
|
110
199
|
[:has_many].include? @macro
|
111
200
|
end
|
112
201
|
|
113
|
-
|
202
|
+
def remove_member(member, owner)
|
203
|
+
collection = owner.attributes[attribute]
|
204
|
+
return if collection.nil?
|
205
|
+
collection.delete(member)
|
206
|
+
end
|
114
207
|
|
208
|
+
def add_member(member, owner)
|
209
|
+
owner.attributes[attribute] ||= ReactiveRecord::Collection.new(owner_class, owner, self)
|
210
|
+
owner.attributes[attribute]._internal_push member
|
211
|
+
end
|
212
|
+
end
|
115
213
|
end
|
116
|
-
|
117
|
-
|
118
214
|
end
|