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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -1
  3. data/.rspec +0 -1
  4. data/Gemfile +6 -6
  5. data/Rakefile +27 -3
  6. data/hyper-model.gemspec +10 -19
  7. data/lib/active_record_base.rb +101 -33
  8. data/lib/hyper-model.rb +4 -2
  9. data/lib/hyper_model/version.rb +1 -1
  10. data/lib/hyper_react/input_tags.rb +2 -1
  11. data/lib/reactive_record/active_record/associations.rb +130 -34
  12. data/lib/reactive_record/active_record/base.rb +17 -0
  13. data/lib/reactive_record/active_record/class_methods.rb +124 -52
  14. data/lib/reactive_record/active_record/error.rb +2 -0
  15. data/lib/reactive_record/active_record/errors.rb +10 -6
  16. data/lib/reactive_record/active_record/instance_methods.rb +74 -6
  17. data/lib/reactive_record/active_record/reactive_record/backing_record_inspector.rb +22 -5
  18. data/lib/reactive_record/active_record/reactive_record/base.rb +56 -30
  19. data/lib/reactive_record/active_record/reactive_record/collection.rb +219 -70
  20. data/lib/reactive_record/active_record/reactive_record/dummy_polymorph.rb +22 -0
  21. data/lib/reactive_record/active_record/reactive_record/dummy_value.rb +27 -15
  22. data/lib/reactive_record/active_record/reactive_record/getters.rb +33 -10
  23. data/lib/reactive_record/active_record/reactive_record/isomorphic_base.rb +73 -46
  24. data/lib/reactive_record/active_record/reactive_record/lookup_tables.rb +5 -5
  25. data/lib/reactive_record/active_record/reactive_record/operations.rb +10 -3
  26. data/lib/reactive_record/active_record/reactive_record/scoped_collection.rb +3 -0
  27. data/lib/reactive_record/active_record/reactive_record/setters.rb +108 -71
  28. data/lib/reactive_record/active_record/reactive_record/while_loading.rb +258 -41
  29. data/lib/reactive_record/broadcast.rb +62 -25
  30. data/lib/reactive_record/interval.rb +3 -3
  31. data/lib/reactive_record/permissions.rb +14 -2
  32. data/lib/reactive_record/scope_description.rb +3 -2
  33. data/lib/reactive_record/server_data_cache.rb +99 -49
  34. data/polymorph-notes.md +143 -0
  35. data/spec_fails.txt +3 -0
  36. metadata +49 -162
  37. data/Gemfile.lock +0 -431
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 45f0eb6698f8f38558c8440ca53cafb694704fd7155df411145f59d79ad5dfa9
4
- data.tar.gz: b952ef89cf6964faf9d2b1619f439f2684435c73d14fa0cb079d64653e964dd2
3
+ metadata.gz: b2f63052beefadb742e008e12976afe889105a0cdbd864ea49902654eb82be51
4
+ data.tar.gz: 2bdf35fb8b4ccd7fd00144be9ba52d8a133797e47825b91cf6acae8dc0673043
5
5
  SHA512:
6
- metadata.gz: 55c5bdc7c4a1c93997d9f05c453446ec959e74585c466765c11457cdd9296d5771265434e49f1ebaad690f3c7404140099812933a80f974c7ba40ce382902a25
7
- data.tar.gz: 02ab7540903efd5818dbb12bfaac9fbac82d643e8edddf4fd3c8600ccddb79c82c325021a78fda51362e49bcfca16d12571505b0b2473183ed47348919bef89a
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
@@ -1,2 +1 @@
1
- --format documentation
2
1
  --color
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).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,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
- spec.add_dependency 'hyper-store', HyperModel::VERSION # only for Hyperstack::Internal::Store::State which should be moved
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
- #spec.add_development_dependency 'hyper-spec', HyperModel::VERSION
42
- spec.add_development_dependency 'mysql2'
43
- spec.add_development_dependency 'opal-activesupport', '~> 0.3.1'
44
- spec.add_development_dependency 'opal-browser', '~> 0.2.0'
45
- spec.add_development_dependency 'opal-rails', '~> 0.9.4'
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', '>= 4.0.0'
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
@@ -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 && 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(_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,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
- 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'
@@ -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"
@@ -1,3 +1,3 @@
1
1
  module HyperModel
2
- VERSION = '1.0.alpha1.1'
2
+ VERSION = '1.0.alpha1.6'
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