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.
Files changed (38) 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 +18 -6
  6. data/hyper-model.gemspec +12 -20
  7. data/lib/active_record_base.rb +95 -28
  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 +125 -35
  13. data/lib/reactive_record/active_record/base.rb +32 -0
  14. data/lib/reactive_record/active_record/class_methods.rb +125 -53
  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 +196 -63
  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 +71 -44
  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 +7 -1
  28. data/lib/reactive_record/active_record/reactive_record/scoped_collection.rb +3 -6
  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 +22 -1
  31. data/lib/reactive_record/broadcast.rb +59 -25
  32. data/lib/reactive_record/interval.rb +3 -3
  33. data/lib/reactive_record/permissions.rb +1 -1
  34. data/lib/reactive_record/scope_description.rb +3 -2
  35. data/lib/reactive_record/server_data_cache.rb +78 -48
  36. data/polymorph-notes.md +143 -0
  37. metadata +52 -157
  38. data/Gemfile.lock +0 -440
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 071e8cc562ebddf1c0a260587a123d91db9f2a7c52d43b06fcf063079d66129f
4
- data.tar.gz: 4cb7539129b7367f3156c0726ac3ed164e1db391f7806457253d303e9940941c
3
+ metadata.gz: caef9d023cb5294a2d4f95feb2bfea4234adc0b780881da6c011d1d765504203
4
+ data.tar.gz: c80e5ed0e014d3013250e0b31cd897e086feb89d916c9967e9a08a9ba4ecbce7
5
5
  SHA512:
6
- metadata.gz: 2c1bcd8fee396593e2f31d072f35cbf88467be2b02c198d523c052b67a211d9fea4501ca86c4f0a388cee35bc34f4ac9156147a13aac9aec4842616a5c3c1772
7
- data.tar.gz: c226e31b1882e96c8a7a080114aea90aacdb0bb77331fc36854f79fe118fabd119f278bc20b94d3693bcc69d7382bc6e4d2f1e91c042b41eb660d2214e75c45d
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
@@ -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,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).each { |batch| Rake::Task["spec:batch#{batch}"].invoke rescue nil }
18
+ run_batches(1..2)
6
19
  end
7
20
 
8
21
  task :part2 do
9
- (3..4).each { |batch| Rake::Task["spec:batch#{batch}"].invoke rescue nil }
22
+ run_batches(3..4)
10
23
  end
11
24
 
12
25
  task :part3 do
13
- (5..7).each { |batch| Rake::Task["spec:batch#{batch}"].invoke rescue nil }
26
+ run_batches(5..7)
14
27
  end
15
28
 
16
29
  task :spec do
17
- (1..7).each { |batch| Rake::Task["spec:batch#{batch}"].invoke rescue nil }
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
- spec.add_development_dependency 'bundler', ['>= 1.17.3', '< 2.1']
33
- spec.add_development_dependency 'capybara'
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
- #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'
@@ -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
- # add secure access for find, find_by, and belongs_to and has_one relations.
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? || previous_changes.empty?
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
 
@@ -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.3'
2
+ VERSION = '1.0.alpha1.8'
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.singularize) || 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,63 +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
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: #{klass}.has_many :#{@owner_class.name.underscore.pluralize}, foreign_key: #{@association_foreign_key}", :warning
104
- klass.has_many @owner_class.name.underscore.pluralize, foreign_key: @association_foreign_key
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: #{klass}.belongs_to :#{@owner_class.name.underscore}, foreign_key: #{@association_foreign_key}", :warning
107
- klass.belongs_to @owner_class.name.underscore, foreign_key: @association_foreign_key
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
- end
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