hyper-model 1.0.alpha1.3 → 1.0.alpha1.8

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 (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