hyper-model 1.0.alpha1.1 → 1.0.alpha1.6
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 +4 -4
- data/.gitignore +4 -1
- data/.rspec +0 -1
- data/Gemfile +6 -6
- data/Rakefile +27 -3
- data/hyper-model.gemspec +10 -19
- data/lib/active_record_base.rb +101 -33
- data/lib/hyper-model.rb +4 -2
- 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 +17 -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 +10 -6
- data/lib/reactive_record/active_record/instance_methods.rb +74 -6
- data/lib/reactive_record/active_record/reactive_record/backing_record_inspector.rb +22 -5
- data/lib/reactive_record/active_record/reactive_record/base.rb +56 -30
- data/lib/reactive_record/active_record/reactive_record/collection.rb +219 -70
- 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 +73 -46
- 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 +108 -71
- data/lib/reactive_record/active_record/reactive_record/while_loading.rb +258 -41
- 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 +49 -162
- data/Gemfile.lock +0 -431
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b2f63052beefadb742e008e12976afe889105a0cdbd864ea49902654eb82be51
|
4
|
+
data.tar.gz: 2bdf35fb8b4ccd7fd00144be9ba52d8a133797e47825b91cf6acae8dc0673043
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2e7481f8e04d3d464507d7c25c20b3dc55167dd6b8cb368f28113e9f52b4ba95815611b8f4fe740d207860f1bc60cdb4ddcdafb996d83ddf2d51afb8cc397fb5
|
7
|
+
data.tar.gz: e0e5fb04e304496c3d0ef8353a9ed0bd36a303257e3f062d9ed61e75b4af8d1ded8ed18defd37e7e44dd79b618b221d129287a39d9e331bb20b9cc648c668124
|
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,11 +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
|
-
gem 'hyper-store', path: '../hyper-store'
|
8
3
|
gem 'hyper-component', path: '../hyper-component'
|
9
4
|
gem 'hyper-operation', path: '../hyper-operation'
|
10
|
-
|
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
|
11
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,32 +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
|
32
|
-
|
31
|
+
|
33
32
|
spec.add_development_dependency 'bundler'
|
34
|
-
spec.add_development_dependency 'capybara'
|
35
|
-
spec.add_development_dependency 'chromedriver-helper', '1.2.0'
|
36
|
-
spec.add_development_dependency 'libv8', '~> 6.3.0' # see https://github.com/discourse/mini_racer/issues/92
|
37
|
-
spec.add_development_dependency 'mini_racer', '~> 0.1.15'
|
38
|
-
spec.add_development_dependency 'selenium-webdriver'
|
39
33
|
spec.add_development_dependency 'database_cleaner'
|
40
34
|
spec.add_development_dependency 'factory_bot_rails'
|
41
|
-
|
42
|
-
spec.add_development_dependency '
|
43
|
-
spec.add_development_dependency '
|
44
|
-
spec.add_development_dependency '
|
45
|
-
spec.add_development_dependency 'opal-rails', '
|
46
|
-
spec.add_development_dependency 'parser'
|
47
|
-
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'
|
48
40
|
spec.add_development_dependency 'pry-rescue'
|
41
|
+
spec.add_development_dependency 'pry-stack_explorer'
|
49
42
|
spec.add_development_dependency 'puma'
|
50
43
|
spec.add_development_dependency 'pusher'
|
51
44
|
spec.add_development_dependency 'pusher-fake'
|
52
|
-
spec.add_development_dependency 'rails', '>=
|
45
|
+
spec.add_development_dependency 'rails', ENV['RAILS_VERSION'] || '>= 5.0.0', '< 7.0'
|
53
46
|
spec.add_development_dependency 'rake'
|
54
47
|
spec.add_development_dependency 'react-rails', '>= 2.4.0', '< 2.5.0'
|
55
|
-
spec.add_development_dependency 'reactrb-rails-generator'
|
56
48
|
spec.add_development_dependency 'rspec-collection_matchers'
|
57
49
|
spec.add_development_dependency 'rspec-expectations'
|
58
50
|
spec.add_development_dependency 'rspec-its'
|
@@ -63,8 +55,7 @@ Gem::Specification.new do |spec|
|
|
63
55
|
spec.add_development_dependency 'rubocop', '~> 0.51.0'
|
64
56
|
spec.add_development_dependency 'shoulda'
|
65
57
|
spec.add_development_dependency 'shoulda-matchers'
|
66
|
-
spec.add_development_dependency 'spring-commands-rspec'
|
67
|
-
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'
|
68
60
|
spec.add_development_dependency 'timecop', '~> 0.8.1'
|
69
|
-
spec.add_development_dependency 'unparser'
|
70
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 && opts[:server].respond_to?(:call)
|
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,37 @@ 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
|
327
395
|
end
|
328
396
|
end
|
329
397
|
|
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'
|
@@ -10,7 +12,6 @@ if RUBY_ENGINE == 'opal'
|
|
10
12
|
require 'object/tap'
|
11
13
|
require 'active_model_client_stubs'
|
12
14
|
require 'hyper-state'
|
13
|
-
require 'hyper-store'
|
14
15
|
require "reactive_record/active_record_error"
|
15
16
|
require "reactive_record/active_record/errors"
|
16
17
|
require "reactive_record/active_record/error"
|
@@ -21,6 +22,7 @@ if RUBY_ENGINE == 'opal'
|
|
21
22
|
require "reactive_record/active_record/reactive_record/isomorphic_base"
|
22
23
|
require 'reactive_record/active_record/reactive_record/dummy_value'
|
23
24
|
require 'reactive_record/active_record/reactive_record/column_types'
|
25
|
+
require 'reactive_record/active_record/reactive_record/dummy_polymorph'
|
24
26
|
require "reactive_record/active_record/aggregations"
|
25
27
|
require "reactive_record/active_record/associations"
|
26
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
|