hyper-model 1.0.alpha1.3 → 1.0.alpha1.8
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 +18 -6
- data/hyper-model.gemspec +12 -20
- data/lib/active_record_base.rb +95 -28
- 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 +125 -35
- data/lib/reactive_record/active_record/base.rb +32 -0
- data/lib/reactive_record/active_record/class_methods.rb +125 -53
- 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 +196 -63
- 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 +71 -44
- data/lib/reactive_record/active_record/reactive_record/lookup_tables.rb +5 -5
- data/lib/reactive_record/active_record/reactive_record/operations.rb +7 -1
- data/lib/reactive_record/active_record/reactive_record/scoped_collection.rb +3 -6
- data/lib/reactive_record/active_record/reactive_record/setters.rb +105 -68
- data/lib/reactive_record/active_record/reactive_record/while_loading.rb +22 -1
- data/lib/reactive_record/broadcast.rb +59 -25
- data/lib/reactive_record/interval.rb +3 -3
- data/lib/reactive_record/permissions.rb +1 -1
- data/lib/reactive_record/scope_description.rb +3 -2
- data/lib/reactive_record/server_data_cache.rb +78 -48
- data/polymorph-notes.md +143 -0
- metadata +52 -157
- data/Gemfile.lock +0 -440
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: caef9d023cb5294a2d4f95feb2bfea4234adc0b780881da6c011d1d765504203
|
4
|
+
data.tar.gz: c80e5ed0e014d3013250e0b31cd897e086feb89d916c9967e9a08a9ba4ecbce7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 610334f5accd3678c94efa95b50614efd4d12685c0b288a17cae7ffef4688635657a23b97605e747cc3606a36632a8803d8f159f027cf9653fd514647ad657fc
|
7
|
+
data.tar.gz: 7a6c022f86b1f9718bb162822f8bece902c649dfcdd8891936afed03e57ab7da70374c9fbc85e3544b0d1cc8f3bdfcd3a266cebf98f26dca80c9a34784d7dd74
|
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,29 +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
|
+
|
4
17
|
task :part1 do
|
5
|
-
(1..2)
|
18
|
+
run_batches(1..2)
|
6
19
|
end
|
7
20
|
|
8
21
|
task :part2 do
|
9
|
-
(3..4)
|
22
|
+
run_batches(3..4)
|
10
23
|
end
|
11
24
|
|
12
25
|
task :part3 do
|
13
|
-
(5..7)
|
26
|
+
run_batches(5..7)
|
14
27
|
end
|
15
28
|
|
16
29
|
task :spec do
|
17
|
-
(1..7)
|
30
|
+
run_batches(1..7)
|
18
31
|
end
|
19
32
|
|
20
33
|
namespace :spec do
|
21
34
|
task :prepare do
|
22
|
-
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)
|
23
36
|
end
|
24
37
|
(1..7).each do |batch|
|
25
38
|
RSpec::Core::RakeTask.new(:"batch#{batch}") do |t|
|
26
|
-
t.fail_on_error = false unless batch == 7
|
27
39
|
t.pattern = "spec/batch#{batch}/**/*_spec.rb"
|
28
40
|
end
|
29
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
|
32
|
-
|
33
|
-
spec.add_development_dependency '
|
34
|
-
spec.add_development_dependency 'chromedriver-helper', '1.2.0'
|
35
|
-
spec.add_development_dependency 'libv8'
|
36
|
-
spec.add_development_dependency 'mini_racer', '~> 0.2.4'
|
37
|
-
spec.add_development_dependency 'selenium-webdriver'
|
31
|
+
|
32
|
+
spec.add_development_dependency 'bundler'
|
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'
|
@@ -87,7 +116,7 @@ module ActiveRecord
|
|
87
116
|
this.acting_user = acting_user
|
88
117
|
# returns a PsuedoRelationArray which will respond to the
|
89
118
|
# __secure_collection_check method
|
90
|
-
ReactiveRecordPsuedoRelationArray.new([this.instance_exec(*args, &block)])
|
119
|
+
ReactiveRecordPsuedoRelationArray.new([*this.instance_exec(*args, &block)])
|
91
120
|
ensure
|
92
121
|
this.acting_user = old
|
93
122
|
end
|
@@ -255,21 +284,7 @@ module ActiveRecord
|
|
255
284
|
pre_syncromesh_has_many name, *args, opts.except(:regulate), &block
|
256
285
|
end
|
257
286
|
|
258
|
-
|
259
|
-
# No explicit security checks are needed here, as the data returned by these objects
|
260
|
-
# will be further processedand checked before returning. I.e. it is not possible to
|
261
|
-
# simply return `find(1)` but if you try returning `find(1).name` the permission system
|
262
|
-
# will check to see if the name attribute can be legally sent to the current acting user.
|
263
|
-
|
264
|
-
def __secure_remote_access_to_find(_self, _acting_user, *args)
|
265
|
-
find(*args)
|
266
|
-
end
|
267
|
-
|
268
|
-
def __secure_remote_access_to_find_by(_self, _acting_user, *args)
|
269
|
-
find_by(*args)
|
270
|
-
end
|
271
|
-
|
272
|
-
%i[belongs_to has_one].each do |macro|
|
287
|
+
%i[belongs_to has_one composed_of].each do |macro|
|
273
288
|
alias_method :"pre_syncromesh_#{macro}", macro
|
274
289
|
define_method(macro) do |name, *aargs, &block|
|
275
290
|
define_method(:"__secure_remote_access_to_#{name}") do |this, _acting_user, *args|
|
@@ -284,6 +299,12 @@ module ActiveRecord
|
|
284
299
|
Hyperstack::InternalPolicy.raise_operation_access_violation(:scoped_denied, "#{self.class} regulation denies scope access. Called from #{caller_locations(1)}")
|
285
300
|
end
|
286
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
|
+
|
287
308
|
# call do_not_synchronize to block synchronization of a model
|
288
309
|
|
289
310
|
def self.do_not_synchronize
|
@@ -300,17 +321,28 @@ module ActiveRecord
|
|
300
321
|
self.class.do_not_synchronize?
|
301
322
|
end
|
302
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
|
+
|
303
334
|
after_commit :synchromesh_after_create, on: [:create]
|
304
335
|
after_commit :synchromesh_after_change, on: [:update]
|
305
336
|
after_commit :synchromesh_after_destroy, on: [:destroy]
|
306
337
|
|
307
338
|
def synchromesh_after_create
|
339
|
+
puts "#{self}.synchromesh_after_create: #{do_not_synchronize?} channels: #{Hyperstack::Connection.active}" if Hyperstack::Connection.show_diagnostics
|
308
340
|
return if do_not_synchronize?
|
309
341
|
ReactiveRecord::Broadcast.after_commit :create, self
|
310
342
|
end
|
311
343
|
|
312
344
|
def synchromesh_after_change
|
313
|
-
return if do_not_synchronize? ||
|
345
|
+
return if do_not_synchronize? || saved_changes.empty?
|
314
346
|
ReactiveRecord::Broadcast.after_commit :change, self
|
315
347
|
end
|
316
348
|
|
@@ -329,6 +361,41 @@ module ActiveRecord
|
|
329
361
|
%i[limit offset].each do |scope|
|
330
362
|
regulate_scope(scope) {}
|
331
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) }
|
332
399
|
end
|
333
400
|
end
|
334
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,63 +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
|
99
172
|
end
|
173
|
+
raise "could not find inverse of polymorphic belongs_to: #{model.inspect} #{self.inspect}" if options[:polymorphic]
|
100
174
|
# instead of raising an error go ahead and create the inverse relationship if it does not exist.
|
101
175
|
# https://github.com/hyperstack-org/hyperstack/issues/89
|
102
176
|
if macro == :belongs_to
|
103
|
-
Hyperstack::Component::IsomorphicHelpers.log "**** warning dynamically adding relationship: #{
|
104
|
-
|
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
|
105
182
|
else
|
106
|
-
Hyperstack::Component::IsomorphicHelpers.log "**** warning dynamically adding relationship: #{
|
107
|
-
|
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
|
108
185
|
end
|
109
186
|
end
|
110
187
|
|
111
|
-
def klass
|
112
|
-
@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
|
113
196
|
end
|
114
197
|
|
115
198
|
def collection?
|
116
199
|
[:has_many].include? @macro
|
117
200
|
end
|
118
201
|
|
119
|
-
|
202
|
+
def remove_member(member, owner)
|
203
|
+
collection = owner.attributes[attribute]
|
204
|
+
return if collection.nil?
|
205
|
+
collection.delete(member)
|
206
|
+
end
|
120
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
|
121
213
|
end
|
122
|
-
|
123
|
-
|
124
214
|
end
|