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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -1
  3. data/.rspec +0 -1
  4. data/Gemfile +6 -5
  5. data/Rakefile +27 -3
  6. data/hyper-model.gemspec +11 -19
  7. data/lib/active_record_base.rb +105 -33
  8. data/lib/enumerable/pluck.rb +3 -2
  9. data/lib/hyper-model.rb +4 -1
  10. data/lib/hyper_model/version.rb +1 -1
  11. data/lib/hyper_react/input_tags.rb +2 -1
  12. data/lib/reactive_record/active_record/associations.rb +130 -34
  13. data/lib/reactive_record/active_record/base.rb +32 -0
  14. data/lib/reactive_record/active_record/class_methods.rb +124 -52
  15. data/lib/reactive_record/active_record/error.rb +2 -0
  16. data/lib/reactive_record/active_record/errors.rb +8 -4
  17. data/lib/reactive_record/active_record/instance_methods.rb +73 -5
  18. data/lib/reactive_record/active_record/public_columns_hash.rb +25 -26
  19. data/lib/reactive_record/active_record/reactive_record/backing_record_inspector.rb +22 -5
  20. data/lib/reactive_record/active_record/reactive_record/base.rb +50 -24
  21. data/lib/reactive_record/active_record/reactive_record/collection.rb +226 -68
  22. data/lib/reactive_record/active_record/reactive_record/dummy_polymorph.rb +22 -0
  23. data/lib/reactive_record/active_record/reactive_record/dummy_value.rb +27 -15
  24. data/lib/reactive_record/active_record/reactive_record/getters.rb +33 -10
  25. data/lib/reactive_record/active_record/reactive_record/isomorphic_base.rb +81 -51
  26. data/lib/reactive_record/active_record/reactive_record/lookup_tables.rb +5 -5
  27. data/lib/reactive_record/active_record/reactive_record/operations.rb +10 -3
  28. data/lib/reactive_record/active_record/reactive_record/scoped_collection.rb +3 -0
  29. data/lib/reactive_record/active_record/reactive_record/setters.rb +105 -68
  30. data/lib/reactive_record/active_record/reactive_record/while_loading.rb +249 -32
  31. data/lib/reactive_record/broadcast.rb +62 -25
  32. data/lib/reactive_record/interval.rb +3 -3
  33. data/lib/reactive_record/permissions.rb +14 -2
  34. data/lib/reactive_record/scope_description.rb +3 -2
  35. data/lib/reactive_record/server_data_cache.rb +99 -49
  36. data/polymorph-notes.md +143 -0
  37. data/spec_fails.txt +3 -0
  38. metadata +54 -153
  39. data/Gemfile.lock +0 -421
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 87c3f5488b0d7290f9fd974234351fae079e3830db6e57e953b54f9063ae41ab
4
- data.tar.gz: cfd1b544e23337620f978df7bb625503bfd2b6cdaa832f874e4dd94a15f02e34
3
+ metadata.gz: d85eab56099328a86bb19e2911f16bd1b5b9a6e98a79d7c912898dce7775e876
4
+ data.tar.gz: cf0f2c03f22c84710e76e5177f8f54f8847c9e1e671f3c5aab38b8c77abbfc8f
5
5
  SHA512:
6
- metadata.gz: e17c81b272167cd39b48b1c63fb7c451100966ede28e886ad2a8e0ddf328a961cb01dd63af6341fc166e8095fed23d0d13e98d589e873fd39d205168851e5cac
7
- data.tar.gz: bb7a6021e96284469580667b58304e3aaa41ae3fdae4196f14123a09d44be53908dbf4ea0165d5edd903132f84a9f8d144629d6292eea55063681a814e8e908b
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
@@ -1,2 +1 @@
1
- --format documentation
2
1
  --color
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).each { |batch| Rake::Task["spec:batch#{batch}"].invoke rescue nil }
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
- #spec.add_development_dependency 'hyper-spec', HyperModel::VERSION
41
- spec.add_development_dependency 'mysql2'
42
- spec.add_development_dependency 'opal-activesupport', '~> 0.3.1'
43
- spec.add_development_dependency 'opal-browser', '~> 0.2.0'
44
- spec.add_development_dependency 'opal-rails', '~> 0.9.4'
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', '>= 4.0.0'
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', '~> 0.51.0'
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
@@ -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
- def self._synchromesh_scope_args_check(args)
9
- opts = if args.count == 2 && args[1].is_a?(Hash)
10
- args[1].merge(server: args[0])
11
- elsif args[0].is_a? Hash
12
- args[0]
13
- else
14
- { server: args[0] }
15
- end
16
- return opts if opts && opts[:server].respond_to?(:call)
17
- raise 'must provide either a proc as the first arg or by the '\
18
- '`:server` option to scope and default_scope methods'
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(_acting_user)
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
- def __secure_collection_check(acting_user)
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(:scoped_permission_not_granted, "Last relation: #{self}, acting_user: #{acting_user}")
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
- # add secure access for find, find_by, and belongs_to and has_one relations.
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? || previous_changes.empty?
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
 
@@ -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(key)
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
- require 'hyper-component'
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"
@@ -1,3 +1,3 @@
1
1
  module HyperModel
2
- VERSION = '1.0.alpha1.2'
2
+ VERSION = '1.0.alpha1.7'
3
3
  end
@@ -42,7 +42,8 @@ module Hyperstack
42
42
  Hyperstack::Internal::Component::RenderingContext.render(tag, opts) { children.each(&:render) }
43
43
  end
44
44
  end
45
- const_set component, klass
45
+
46
+ Object.const_set component, klass
46
47
  end
47
48
  end
48
49
  end
@@ -3,7 +3,7 @@ module ActiveRecord
3
3
  class Base
4
4
 
5
5
  def self.reflect_on_all_associations
6
- base_class.instance_eval { @associations ||= superclass.instance_eval { (@associations && @associations.dup) || [] } }
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
- @klass_name = options[:class_name] || (collection? && name.camelize.sub(/s$/, '')) || name.camelize
48
- @association_foreign_key = options[:foreign_key] || (macro == :belongs_to && "#{name}_id") || "#{@owner_class.name.underscore}_id"
49
- @source = options[:source] || @klass_name.underscore if options[:through]
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
- def through_associations
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
- @through_associations ||= klass.reflect_on_all_associations.select do |assoc|
69
- assoc.through_association && assoc.inverse == self
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 source_associations
74
- # find all associations that use this association as the source
75
- # that is final all associations that are using this association as the source in a
76
- # through relationship
77
- @source_associations ||= owner_class.reflect_on_all_associations.collect do |sibling|
78
- sibling.klass.reflect_on_all_associations.select do |assoc|
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 inverse
85
- @inverse ||=
86
- through_association ? through_association.inverse : find_inverse
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
- def inverse_of
90
- @inverse_of ||= inverse.attribute
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
- klass.reflect_on_all_associations.each do |association|
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 == association.owner_class
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
- end
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