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
@@ -6,39 +6,38 @@ module ActiveRecord
6
6
  # adds method to get the HyperMesh public column types
7
7
  # this works because the public folder is currently required to be eager loaded.
8
8
  class Base
9
+ @@hyper_stack_public_columns_hash_mutex = Mutex.new
9
10
  def self.public_columns_hash
10
- return @public_columns_hash if @public_columns_hash && Rails.env.production?
11
- files = []
12
- Hyperstack.public_model_directories.each do |dir|
13
- dir_length = Rails.root.join(dir).to_s.length + 1
14
- Dir.glob(Rails.root.join(dir, '**', '*.rb')).each do |file|
15
- require_dependency(file) # still the file is loaded to make sure for development and test env
16
- files << file[dir_length..-4]
11
+ @@hyper_stack_public_columns_hash_mutex.synchronize do
12
+ return @public_columns_hash if @public_columns_hash && Rails.env.production?
13
+ files = []
14
+ Hyperstack.public_model_directories.each do |dir|
15
+ dir_length = Rails.root.join(dir).to_s.length + 1
16
+ Dir.glob(Rails.root.join(dir, '**', '*.rb')).each do |file|
17
+ require_dependency(file) # still the file is loaded to make sure for development and test env
18
+ files << file[dir_length..-4]
19
+ end
17
20
  end
18
- end
19
- @public_columns_hash = {}
20
- # descendants only works for already loaded models!
21
- descendants.each do |model|
22
- if files.include?(model.name.underscore) && model.name.underscore != 'application_record'
23
- @public_columns_hash[model.name] = model.columns_hash rescue nil # why rescue?
21
+ @public_columns_hash = {}
22
+ # descendants only works for already loaded models!
23
+ descendants.each do |model|
24
+ if files.include?(model.name.underscore) && model.name.underscore != 'application_record'
25
+ @public_columns_hash[model.name] = model.columns_hash rescue nil # why rescue?
26
+ end
24
27
  end
25
- # begin
26
- # @public_columns_hash[model.name] = model.columns_hash if model.table_name
27
- # rescue Exception => e
28
- # binding.pry
29
- # @public_columns_hash = nil
30
- # raise $!, "Could not read 'columns_hash' for #{model}: #{$!}", $!.backtrace
31
- # end if files.include?(model.name.underscore) && model.name.underscore != 'application_record'
28
+ @public_columns_hash
32
29
  end
33
- @public_columns_hash
34
30
  end
35
31
 
32
+ @@hyper_stack_public_columns_hash_as_json_mutex = Mutex.new
36
33
  def self.public_columns_hash_as_json
37
- return @public_columns_hash_json if @public_columns_hash_json && Rails.env.production?
38
- pch = public_columns_hash
39
- return @public_columns_hash_json if @prev_public_columns_hash == pch
40
- @prev_public_columns_hash = pch
41
- @public_columns_hash_json = pch.to_json
34
+ @@hyper_stack_public_columns_hash_as_json_mutex.synchronize do
35
+ return @public_columns_hash_json if @public_columns_hash_json && Rails.env.production?
36
+ pch = public_columns_hash
37
+ return @public_columns_hash_json if @prev_public_columns_hash == pch
38
+ @prev_public_columns_hash = pch
39
+ @public_columns_hash_json = pch.to_json
40
+ end
42
41
  end
43
42
  end
44
43
  end
@@ -4,11 +4,11 @@ module ReactiveRecord
4
4
  # the appropriate string. The order of execution is important!
5
5
  module BackingRecordInspector
6
6
  def inspection_details
7
- return error_details unless errors.empty?
8
- return new_details if new?
7
+ return error_details unless errors.empty?
8
+ return new_details if new?
9
9
  return destroyed_details if destroyed
10
- return loading_details unless @attributes.key? primary_key
11
- return dirty_details unless changed_attributes.empty?
10
+ return loading_details unless @attributes.key? primary_key
11
+ return dirty_details unless changed_attributes.empty?
12
12
  "[loaded id: #{id}]"
13
13
  end
14
14
 
@@ -26,11 +26,28 @@ module ReactiveRecord
26
26
  end
27
27
 
28
28
  def loading_details
29
- "[loading #{vector}]"
29
+ "[loading #{pretty_vector}]"
30
30
  end
31
31
 
32
32
  def dirty_details
33
33
  "[changed id: #{id} #{changes}]"
34
34
  end
35
+
36
+ def pretty_vector
37
+ v = []
38
+ i = 0
39
+ while i < vector.length
40
+ if vector[i] == 'all' && vector[i + 1].is_a?(Array) &&
41
+ vector[i + 1][0] == '___hyperstack_internal_scoped_find_by' &&
42
+ vector[i + 2] == '*0'
43
+ v << ['find_by', vector[i + 1][1]]
44
+ i += 3
45
+ else
46
+ v << vector[i]
47
+ i += 1
48
+ end
49
+ end
50
+ v
51
+ end
35
52
  end
36
53
  end
@@ -38,6 +38,7 @@ module ReactiveRecord
38
38
  attr_accessor :aggregate_owner
39
39
  attr_accessor :aggregate_attribute
40
40
  attr_accessor :destroyed
41
+ attr_accessor :being_destroyed
41
42
  attr_accessor :updated_during
42
43
  attr_accessor :synced_attributes
43
44
  attr_accessor :virgin
@@ -67,30 +68,35 @@ module ReactiveRecord
67
68
  load_data { ServerDataCache.load_from_json(json, target) }
68
69
  end
69
70
 
71
+ def self.find_locally(model, attrs, new_only: nil)
72
+ if (id_to_find = attrs[model.primary_key])
73
+ !new_only && lookup_by_id(model, id_to_find)
74
+ else
75
+ @records[model.base_class].detect do |r|
76
+ (r.new? || !new_only) &&
77
+ !attrs.detect { |attr, value| r.synced_attributes[attr] != value }
78
+ end
79
+ end
80
+ end
81
+
82
+ def self.find_by_id(model, id)
83
+ find(model, model.primary_key => id)
84
+ end
85
+
70
86
  def self.find(model, attrs)
71
87
  # will return the unique record with this attribute-value pair
72
88
  # value cannot be an association or aggregation
73
89
 
74
90
  # add the inheritance column if this is an STI subclass
75
91
 
76
- inher_col = model.inheritance_column
77
- if inher_col && model < model.base_class && !attrs.key?(inher_col)
78
- attrs = attrs.merge(inher_col => model.model_name.to_s)
79
- end
92
+ attrs = model.__hyperstack_preprocess_attrs(attrs)
80
93
 
81
94
  model = model.base_class
82
95
  primary_key = model.primary_key
83
96
 
84
97
  # already have a record with these attribute-value pairs?
85
98
 
86
- record =
87
- if (id_to_find = attrs[primary_key])
88
- lookup_by_id(model, id_to_find)
89
- else
90
- @records[model].detect do |r|
91
- !attrs.detect { |attr, value| r.synced_attributes[attr] != value }
92
- end
93
- end
99
+ record = find_locally(model, attrs)
94
100
 
95
101
  unless record
96
102
  # if not, and then the record may be loaded, but not have this attribute set yet,
@@ -102,10 +108,8 @@ module ReactiveRecord
102
108
  attrs = attrs.merge primary_key => id
103
109
  end
104
110
  # if we don't have a record then create one
105
- # (record = new(model)).vector = [model, [:find_by, attribute => value]] unless record
106
- record ||= set_vector_lookup(new(model), [model, [:find_by, attrs]])
107
- # and set the values
108
- attrs.each { |attr, value| record.sync_attribute(attr, value) }
111
+ record ||= set_vector_lookup(new(model), [model, *find_by_vector(attrs)])
112
+ record.sync_attributes(attrs)
109
113
  end
110
114
  # finally initialize and return the ar_instance
111
115
  record.set_ar_instance!
@@ -115,6 +119,9 @@ module ReactiveRecord
115
119
  # this is the equivilent of find but for associations and aggregations
116
120
  # because we are not fetching a specific attribute yet, there is NO communication with the
117
121
  # server. That only happens during find.
122
+
123
+ return DummyPolymorph.new(vector) unless model
124
+
118
125
  model = model.base_class
119
126
 
120
127
  # do we already have a record with this vector? If so return it, otherwise make a new one.
@@ -122,7 +129,6 @@ module ReactiveRecord
122
129
  # record = @records[model].detect { |record| record.vector == vector }
123
130
  record = lookup_by_vector(vector)
124
131
  unless record
125
-
126
132
  record = new model
127
133
  set_vector_lookup(record, vector)
128
134
  end
@@ -145,7 +151,7 @@ module ReactiveRecord
145
151
  @attributes = {}
146
152
  @changed_attributes = []
147
153
  @virgin = true
148
- records[model] << self
154
+ records[model.base_class] << self
149
155
  Base.set_object_id_lookup(self)
150
156
  end
151
157
 
@@ -175,6 +181,7 @@ module ReactiveRecord
175
181
  @ar_instance.instance_variable_set(:@backing_record, existing_record)
176
182
  existing_record.attributes.merge!(attributes) { |key, v1, v2| v1 }
177
183
  end
184
+ @id = value
178
185
  value
179
186
  end
180
187
 
@@ -201,7 +208,7 @@ module ReactiveRecord
201
208
  end
202
209
 
203
210
  def errors
204
- @errors ||= ActiveModel::Errors.new(self)
211
+ @errors ||= ActiveModel::Errors.new(ar_instance)
205
212
  end
206
213
 
207
214
  # called when we have a newly created record, to initialize
@@ -211,7 +218,7 @@ module ReactiveRecord
211
218
 
212
219
  def initialize_collections
213
220
  if (!vector || vector.empty?) && id && id != ''
214
- Base.set_vector_lookup(self, [@model, [:find_by, @model.primary_key => id]])
221
+ Base.set_vector_lookup(self, [@model, *find_by_vector(@model.primary_key => id)])
215
222
  end
216
223
  Base.load_data do
217
224
  @model.reflect_on_all_associations.each do |assoc|
@@ -247,12 +254,16 @@ module ReactiveRecord
247
254
  return if @create_sync
248
255
  @create_sync = true
249
256
  end
250
- model.unscoped << ar_instance
257
+ model.unscoped._internal_push ar_instance
251
258
  @synced_with_unscoped = !@synced_with_unscoped
252
259
  end
253
260
 
254
- def sync_attribute(attribute, value)
261
+ def sync_attributes(attrs)
262
+ attrs.each { |attr, value| sync_attribute(attr, value) }
263
+ self
264
+ end
255
265
 
266
+ def sync_attribute(attribute, value)
256
267
  @synced_attributes[attribute] = @attributes[attribute] = value
257
268
  Base.set_id_lookup(self) if attribute == primary_key
258
269
 
@@ -260,7 +271,7 @@ module ReactiveRecord
260
271
 
261
272
  if value.is_a? Collection
262
273
  @synced_attributes[attribute] = value.dup_for_sync
263
- elsif aggregation = model.reflect_on_aggregation(attribute) and (aggregation.klass < ActiveRecord::Base)
274
+ elsif (aggregation = model.reflect_on_aggregation(attribute)) && (aggregation.klass < ActiveRecord::Base)
264
275
  value.backing_record.sync!
265
276
  elsif aggregation
266
277
  @synced_attributes[attribute] = aggregation.deserialize(aggregation.serialize(value))
@@ -278,6 +289,14 @@ module ReactiveRecord
278
289
  Base.lookup_by_id(model, id)
279
290
  end
280
291
 
292
+ def id_loaded?
293
+ @id
294
+ end
295
+
296
+ def loaded_id=(id)
297
+ @id = id
298
+ end
299
+
281
300
  def revert
282
301
  @changed_attributes.dup.each do |attribute|
283
302
  @ar_instance.send("#{attribute}=", @synced_attributes[attribute])
@@ -292,9 +311,11 @@ module ReactiveRecord
292
311
  @saving = true
293
312
  end
294
313
 
295
- def errors!(hash)
314
+ def errors!(hash, saving = false)
315
+ @errors_at_last_sync = hash if saving
296
316
  notify_waiting_for_save
297
317
  errors.clear && return unless hash
318
+ errors.non_reactive_clear
298
319
  hash.each do |attribute, messages|
299
320
  messages.each do |message|
300
321
  errors.add(attribute, message)
@@ -302,6 +323,10 @@ module ReactiveRecord
302
323
  end
303
324
  end
304
325
 
326
+ def revert_errors!
327
+ errors!(@errors_at_last_sync)
328
+ end
329
+
305
330
  def saved!(save_only = nil) # sets saving to false AND notifies
306
331
  notify_waiting_for_save
307
332
  return self if save_only
@@ -403,6 +428,7 @@ module ReactiveRecord
403
428
 
404
429
  def destroy_associations
405
430
  @destroyed = false
431
+ @being_destroyed = true
406
432
  model.reflect_on_all_associations.each do |association|
407
433
  if association.collection?
408
434
  @attributes[association.attribute].replace([]) if @attributes[association.attribute]
@@ -29,7 +29,7 @@ module ReactiveRecord
29
29
  @owner = owner # can be nil if this is an outer most scope
30
30
  @association = association
31
31
  @target_klass = target_klass
32
- if owner and !owner.id and vector.length <= 1
32
+ if owner && !owner.id && vector.length <= 1
33
33
  @collection = []
34
34
  elsif vector.length > 0
35
35
  @vector = vector
@@ -52,14 +52,19 @@ module ReactiveRecord
52
52
  def all
53
53
  observed
54
54
  @dummy_collection.notify if @dummy_collection
55
+ # unless false && @collection # this fixes https://github.com/hyperstack-org/hyperstack/issues/82 in very limited cases, and breaks otherthings
56
+ # sync_collection_with_parent
57
+ # end
55
58
  unless @collection
56
59
  @collection = []
57
60
  if ids = ReactiveRecord::Base.fetch_from_db([*@vector, "*all"])
58
61
  ids.each do |id|
59
- @collection << @target_klass.find_by(@target_klass.primary_key => id)
62
+ @collection << ReactiveRecord::Base.find_by_id(@target_klass, id)
60
63
  end
61
64
  else
62
65
  @dummy_collection = ReactiveRecord::Base.load_from_db(nil, *@vector, "*all")
66
+ # this calls back to all now that the collection is initialized,
67
+ # so it has the side effect of creating a dummy value in collection[0]
63
68
  @dummy_record = self[0]
64
69
  end
65
70
  end
@@ -72,6 +77,8 @@ module ReactiveRecord
72
77
  (@collection.length..index).each do |i|
73
78
  new_dummy_record = ReactiveRecord::Base.new_from_vector(@target_klass, nil, *@vector, "*#{i}")
74
79
  new_dummy_record.attributes[@association.inverse_of] = @owner if @association && !@association.through_association?
80
+ # HMT-TODO: the above needs to be looked into... if we are a hmt then don't we need to create a dummy on the joins collection as well?
81
+ # or maybe this just does not work for HMT?
75
82
  @collection << new_dummy_record
76
83
  end
77
84
  end
@@ -80,8 +87,15 @@ module ReactiveRecord
80
87
 
81
88
  def ==(other_collection)
82
89
  observed
83
- return !@collection unless other_collection.is_a? Collection
90
+ # handle special case of other_collection NOT being a collection (typically nil)
91
+ return (@collection || []) == other_collection unless other_collection.is_a? Collection
84
92
  other_collection.observed
93
+ # if either collection has not been created then compare the vectors
94
+ # https://github.com/hyperstack-org/hyperstack/issues/81
95
+ # TODO: if this works then remove the || [] below (2 of them)
96
+ if !@collection || !other_collection.collection
97
+ return @vector == other_collection.vector && unsaved_children == other_collection.unsaved_children
98
+ end
85
99
  my_children = (@collection || []).select { |target| target != @dummy_record }
86
100
  if other_collection
87
101
  other_children = (other_collection.collection || []).select { |target| target != other_collection.dummy_record }
@@ -93,7 +107,7 @@ module ReactiveRecord
93
107
  end
94
108
  # todo move following to a separate module related to scope updates ******************
95
109
  attr_reader :vector
96
- attr_writer :scope_description
110
+ attr_accessor :scope_description
97
111
  attr_writer :parent
98
112
  attr_reader :pre_sync_related_records
99
113
 
@@ -127,7 +141,10 @@ To determine this sync_scopes first asks if the record being changed is in the s
127
141
 
128
142
 
129
143
  =end
144
+ attr_accessor :broadcast_updated_at
145
+
130
146
  def sync_scopes(broadcast)
147
+ self.broadcast_updated_at = broadcast.updated_at
131
148
  # record_with_current_values will return nil if data between
132
149
  # the broadcast record and the value on the client is out of sync
133
150
  # not running set_pre_sync_related_records will cause sync scopes
@@ -145,6 +162,8 @@ To determine this sync_scopes first asks if the record being changed is in the s
145
162
  )
146
163
  record.backing_record.sync_unscoped_collection! if record.destroyed? || broadcast.new?
147
164
  end
165
+ ensure
166
+ self.broadcast_updated_at = nil
148
167
  end
149
168
 
150
169
  def apply_to_all_collections(method, record, dont_gather)
@@ -200,7 +219,7 @@ To determine this sync_scopes first asks if the record being changed is in the s
200
219
  return [] unless attrs[@association.inverse_of] == @owner
201
220
  if !@association.through_association
202
221
  [record]
203
- elsif (source = attrs[@association.source])
222
+ elsif (source = attrs[@association.source]) && source.is_a?(@target_klass)
204
223
  [source]
205
224
  else
206
225
  []
@@ -212,7 +231,6 @@ To determine this sync_scopes first asks if the record being changed is in the s
212
231
  end
213
232
 
214
233
  def filter_records(related_records)
215
- # possibly we should never get here???
216
234
  scope_args = @vector.last.is_a?(Array) ? @vector.last[1..-1] : []
217
235
  @scope_description.filter_records(related_records, scope_args)
218
236
  end
@@ -221,16 +239,23 @@ To determine this sync_scopes first asks if the record being changed is in the s
221
239
  @live_scopes ||= Set.new
222
240
  end
223
241
 
242
+ def in_this_collection(related_records)
243
+ # HMT-TODO: I don't think we can get a set of related records here with a through association unless they are part of the collection
244
+ return related_records if !@association || @association.through_association?
245
+ related_records.select do |r|
246
+ r.backing_record.attributes[@association.inverse_of] == @owner
247
+ end
248
+ end
249
+
224
250
  def set_pre_sync_related_records(related_records, _record = nil)
225
- #related_records = related_records.intersection([*@collection]) <- deleting this works
226
- @pre_sync_related_records = related_records #in_this_collection related_records <- not sure if this works
251
+ @pre_sync_related_records = in_this_collection(related_records)
227
252
  live_scopes.each { |scope| scope.set_pre_sync_related_records(@pre_sync_related_records) }
228
253
  end
229
254
 
230
255
  # NOTE sync_scopes is overridden in scope_description.rb
231
256
  def sync_scopes(related_records, record, filtering = true)
232
257
  #related_records = related_records.intersection([*@collection])
233
- #related_records = in_this_collection related_records
258
+ related_records = in_this_collection(related_records) if filtering
234
259
  live_scopes.each { |scope| scope.sync_scopes(related_records, record, filtering) }
235
260
  notify_of_change unless related_records.empty?
236
261
  ensure
@@ -261,11 +286,15 @@ To determine this sync_scopes first asks if the record being changed is in the s
261
286
  end
262
287
 
263
288
  def link_to_parent
264
- return if @linked
289
+ # puts "#{self}.link_to_parent @linked = #{!!@linked}, collection? #{!!@collection}"
290
+ # always check that parent is synced - fixes issue https://github.com/hyperstack-org/hyperstack/issues/82
291
+ # note that sync_collection_with_parent checks to make sure that is NOT a collection and that there IS a parent
292
+
293
+ return sync_collection_with_parent if @linked
265
294
  @linked = true
266
295
  if @parent
267
296
  @parent.link_child self
268
- sync_collection_with_parent unless collection
297
+ sync_collection_with_parent
269
298
  else
270
299
  ReactiveRecord::Base.add_to_outer_scopes self
271
300
  end
@@ -278,14 +307,23 @@ To determine this sync_scopes first asks if the record being changed is in the s
278
307
  end
279
308
 
280
309
  def sync_collection_with_parent
310
+ # puts "#{self}.sync_collection_with_parent"
311
+ return if @collection || !@parent || @parent.dummy_collection # fixes issue https://github.com/hyperstack-org/hyperstack/issues/78 and supports /82
281
312
  if @parent.collection
313
+ # puts ">>> @parent.collection present"
282
314
  if @parent.collection.empty?
315
+ # puts ">>>>> @parent.collection is empty!"
283
316
  @collection = []
284
317
  elsif filter?
285
- @collection = filter_records(@parent.collection)
318
+ # puts "#{self}.sync_collection_with_parent (@parent = #{@parent}) calling filter records on (#{@parent.collection})"
319
+ @collection = filter_records(@parent.collection).to_a
286
320
  end
287
- elsif @parent._count_internal(false).zero? # just changed this from count.zero?
321
+ elsif !@linked && @parent._count_internal(false).zero?
322
+ # don't check _count_internal if already linked as this cause an unnecessary rendering cycle
323
+ # puts ">>> @parent._count_internal(false).zero? is true!"
288
324
  @count = 0
325
+ else
326
+ # puts ">>> NOP"
289
327
  end
290
328
  end
291
329
 
@@ -303,31 +341,33 @@ To determine this sync_scopes first asks if the record being changed is in the s
303
341
  end
304
342
 
305
343
  def observed
306
- return if @observing || ReactiveRecord::Base.data_loading?
344
+ return self if @observing || ReactiveRecord::Base.data_loading?
307
345
  begin
308
346
  @observing = true
309
347
  link_to_parent
310
348
  reload_from_db(true) if @out_of_date
311
349
  Hyperstack::Internal::State::Variable.get(self, :collection)
350
+ self
312
351
  ensure
313
352
  @observing = false
314
353
  end
315
354
  end
316
355
 
317
- def set_count_state(val)
356
+ def count_state=(val)
318
357
  unless ReactiveRecord::WhileLoading.observed?
319
358
  Hyperstack::Internal::State::Variable.set(self, :collection, collection, true)
320
359
  end
360
+ @count_updated_at = ReactiveRecord::Operations::Base.last_response_sent_at
321
361
  @count = val
322
362
  end
323
363
 
324
-
325
-
326
364
  def _count_internal(load_from_client)
327
365
  # when count is called on a leaf, count_internal is called for each
328
366
  # ancestor. Only the outermost count has load_from_client == true
329
367
  observed
330
- if @collection
368
+ if @count && @dummy_collection
369
+ @count # fixes https://github.com/hyperstack-org/hyperstack/issues/79
370
+ elsif @collection
331
371
  @collection.count
332
372
  elsif @count ||= ReactiveRecord::Base.fetch_from_db([*@vector, "*count"])
333
373
  @count
@@ -366,7 +406,7 @@ To determine this sync_scopes first asks if the record being changed is in the s
366
406
  # child.test_model = 1
367
407
  # so... we go back starting at this collection and look for the first
368
408
  # collection with an owner... that is our guy
369
- child = proxy_association.klass.find(id)
409
+ child = ReactiveRecord::Base.find_by_id(proxy_association.klass, id)
370
410
  push child
371
411
  set_belongs_to child
372
412
  end
@@ -374,7 +414,11 @@ To determine this sync_scopes first asks if the record being changed is in the s
374
414
  def set_belongs_to(child)
375
415
  if @owner
376
416
  # TODO this is major broken...current
377
- child.send("#{@association.inverse_of}=", @owner) if @association && !@association.through_association
417
+ if (through_association = @association.through_association)
418
+ # HMT-TODO: create a new record with owner and child
419
+ else
420
+ child.send("#{@association.inverse_of}=", @owner) if @association && !@association.through_association
421
+ end
378
422
  elsif @parent
379
423
  @parent.set_belongs_to(child)
380
424
  end
@@ -389,36 +433,57 @@ To determine this sync_scopes first asks if the record being changed is in the s
389
433
 
390
434
  def update_child(item)
391
435
  backing_record = item.backing_record
392
- if backing_record && @owner && @association && !@association.through_association? && item.attributes[@association.inverse_of] != @owner
436
+ # HMT TODO: The following && !association.through_association was commented out, causing wrong class items to be added to
437
+ # associations
438
+ # Why was it commented out.
439
+ if backing_record && @owner && @association && item.attributes[@association.inverse_of] != @owner && !@association.through_association?
393
440
  inverse_of = @association.inverse_of
394
- current_association = item.attributes[inverse_of]
441
+ current_association_value = item.attributes[inverse_of]
395
442
  backing_record.virgin = false unless backing_record.data_loading?
443
+ # next line was commented out and following line was active.
396
444
  backing_record.update_belongs_to(inverse_of, @owner)
397
- if current_association && current_association.attributes[@association.attribute]
398
- current_association.attributes[@association.attribute].delete(item)
399
- end
445
+ #backing_record.set_belongs_to_via_has_many(@association, @owner)
446
+ # following is handled by update_belongs_to and is redundant
447
+ # unless current_association_value.nil? # might be a dummy value which responds to nil
448
+ # current_association = @association.inverse.inverse(current_association_value)
449
+ # current_association_attribute = current_association.attribute
450
+ # if current_association.collection? && current_association_value.attributes[current_association_attribute]
451
+ # current_association.attributes[current_association_attribute].delete(item)
452
+ # end
453
+ # end
400
454
  @owner.backing_record.sync_has_many(@association.attribute)
401
455
  end
402
456
  end
403
457
 
404
458
  def push(item)
405
- item.itself # force get of at least the id
406
- if collection
407
- self.force_push item
459
+ if (through_association = @association&.through_association)
460
+ through_association.klass.create(@association.inverse_of => @owner, @association.source => item)
461
+ self
408
462
  else
409
- unsaved_children << item
410
- update_child(item)
411
- @owner.backing_record.sync_has_many(@association.attribute) if @owner && @association
412
- if !@count.nil?
413
- @count += item.destroyed? ? -1 : 1
414
- notify_of_change self
415
- end
463
+ _internal_push(item)
416
464
  end
417
- self
418
465
  end
419
466
 
420
467
  alias << push
421
468
 
469
+ def _internal_push(item)
470
+ insure_sync do
471
+ item.itself # force get of at least the id
472
+ if collection
473
+ self.force_push item
474
+ else
475
+ unsaved_children << item
476
+ update_child(item)
477
+ @owner.backing_record.sync_has_many(@association.attribute) if @owner && @association
478
+ if !@count.nil?
479
+ @count += (item.destroyed? ? -1 : 1)
480
+ notify_of_change self
481
+ end
482
+ end
483
+ end
484
+ self
485
+ end
486
+
422
487
  def sort!(*args, &block)
423
488
  replace(sort(*args, &block))
424
489
  end
@@ -438,13 +503,29 @@ To determine this sync_scopes first asks if the record being changed is in the s
438
503
  notify_of_change self
439
504
  end
440
505
 
441
- [:first, :last].each do |method|
442
- define_method method do |*args|
443
- if args.count == 0
444
- all.send(method)
445
- else
446
- apply_scope(method, *args)
447
- end
506
+ # [:first, :last].each do |method|
507
+ # define_method method do |*args|
508
+ # if args.count == 0
509
+ # all.send(method)
510
+ # else
511
+ # apply_scope(method, *args)
512
+ # end
513
+ # end
514
+ # end
515
+
516
+ def first(n = nil)
517
+ if n
518
+ apply_scope(:first, n)
519
+ else
520
+ self[0]
521
+ end
522
+ end
523
+
524
+ def last(n = nil)
525
+ if n
526
+ apply_scope(:__hyperstack_internal_scoped_last_n, n)
527
+ else
528
+ __hyperstack_internal_scoped_last
448
529
  end
449
530
  end
450
531
 
@@ -466,46 +547,73 @@ To determine this sync_scopes first asks if the record being changed is in the s
466
547
  @dummy_collection.notify
467
548
  array = new_array.is_a?(Collection) ? new_array.collection : new_array
468
549
  @collection.each_with_index do |r, i|
469
- r.id = new_array[i].id if array[i] and array[i].id and !r.new? and r.backing_record.vector.last =~ /^\*[0-9]+$/
550
+ r.id = new_array[i].id if array[i] and array[i].id and !r.new_record? and r.backing_record.vector.last =~ /^\*[0-9]+$/
470
551
  end
471
552
  end
472
-
473
- @collection.dup.each { |item| delete(item) } if @collection # this line is a big nop I think
553
+ # the following makes sure that the existing elements are properly removed from the collection
554
+ @collection.dup.each { |item| delete(item) } if @collection
474
555
  @collection = []
475
556
  if new_array.is_a? Collection
476
557
  @dummy_collection = new_array.dummy_collection
477
558
  @dummy_record = new_array.dummy_record
478
- new_array.collection.each { |item| self << item } if new_array.collection
559
+ new_array.collection.each { |item| _internal_push item } if new_array.collection
479
560
  else
480
561
  @dummy_collection = @dummy_record = nil
481
- new_array.each { |item| self << item }
562
+ new_array.each { |item| _internal_push item }
482
563
  end
483
564
  notify_of_change new_array
484
565
  end
485
566
 
486
- def delete(item)
487
- unsaved_children.delete(item)
488
- notify_of_change(
489
- if @owner && @association && !@association.through_association?
567
+ def destroy_non_habtm(item)
568
+ Hyperstack::Internal::State::Mapper.bulk_update do
569
+ unsaved_children.delete(item)
570
+ if @owner && @association
490
571
  inverse_of = @association.inverse_of
491
- if (backing_record = item.backing_record) && item.attributes[inverse_of] == @owner
572
+ if (backing_record = item.backing_record) && item.attributes[inverse_of] == @owner && !@association.through_association?
492
573
  # the if prevents double update if delete is being called from << (see << above)
493
574
  backing_record.update_belongs_to(inverse_of, nil)
494
575
  end
495
576
  delete_internal(item) { @owner.backing_record.sync_has_many(@association.attribute) }
496
577
  else
497
578
  delete_internal(item)
498
- end
579
+ end.tap { Hyperstack::Internal::State::Variable.set(self, :collection, collection) }
580
+ end
581
+ end
582
+
583
+ def destroy(item)
584
+ return destroy_non_habtm(item) unless @association&.habtm?
585
+
586
+ ta = @association.through_association
587
+ item_foreign_key = @association.source_belongs_to_association.association_foreign_key
588
+ join_record = ta.klass.find_by(
589
+ ta.association_foreign_key => @owner.id,
590
+ item_foreign_key => item.id
499
591
  )
592
+ return destroy_non_habtm(item) if join_record.nil? ||
593
+ join_record.backing_record.being_destroyed
594
+
595
+ join_record&.destroy
500
596
  end
501
597
 
598
+ def insure_sync
599
+ if Collection.broadcast_updated_at && @count_updated_at && Collection.broadcast_updated_at < @count_updated_at
600
+ reload_from_db
601
+ else
602
+ yield
603
+ end
604
+ end
605
+
606
+ alias delete destroy
607
+
502
608
  def delete_internal(item)
503
- if collection
504
- all.delete(item)
505
- elsif !@count.nil?
506
- @count -= 1
609
+ insure_sync do
610
+ if collection
611
+ all.delete(item)
612
+ elsif !@count.nil?
613
+ @count -= 1
614
+ end
615
+ yield if block_given? # was yield item, but item is not used
507
616
  end
508
- yield if block_given? # was yield item, but item is not used
509
617
  item
510
618
  end
511
619
 
@@ -514,23 +622,74 @@ To determine this sync_scopes first asks if the record being changed is in the s
514
622
  @dummy_collection.loading?
515
623
  end
516
624
 
625
+ def find_by(attrs)
626
+ attrs = @target_klass.__hyperstack_preprocess_attrs(attrs)
627
+ (r = __hyperstack_internal_scoped_find_by(attrs)) || return
628
+ r.backing_record.sync_attributes(attrs).set_ar_instance!
629
+ end
630
+
631
+ def find(*args)
632
+ args = args[0] if args[0].is_a? Array
633
+ return args.collect { |id| find(id) } if args.count > 1
634
+ find_by(@target_klass.primary_key => args[0])
635
+ end
636
+
637
+ def _find_by_initializer(scope, attrs)
638
+ found =
639
+ if scope.is_a? Collection
640
+ scope.parent.collection&.detect { |lr| !attrs.detect { |k, v| lr.attributes[k] != v } }
641
+ else
642
+ ReactiveRecord::Base.find_locally(@target_klass, attrs)&.ar_instance
643
+ end
644
+ return first unless found
645
+ @collection = [found]
646
+ found
647
+ end
648
+
649
+ # to avoid fetching the entire collection array we check empty and any against the count
650
+
517
651
  def empty?
518
- # should be handled by method missing below, but opal-rspec does not deal well
519
- # with method missing, so to test...
520
- all.empty?
652
+ count.zero?
653
+ end
654
+
655
+ def any?(*args, &block)
656
+ # If there are any args passed in, then the collection is being used in the condition
657
+ # and we must load it all into memory.
658
+ return all.any?(*args, &block) if args&.length&.positive? || block.present?
659
+
660
+ # Otherwise we can just check the count for efficiency
661
+ !empty?
662
+ end
663
+
664
+ def none?(*args, &block)
665
+ # If there are any args passed in, then the collection is being used in the condition
666
+ # and we must load it all into memory.
667
+ return all.none?(*args, &block) if args&.length&.positive? || block.present?
668
+
669
+ # Otherwise we can just check the count for efficiency
670
+ empty?
521
671
  end
522
672
 
523
673
  def method_missing(method, *args, &block)
524
- if [].respond_to? method
674
+ if args.count == 1 && method.start_with?('find_by_')
675
+ find_by(method.sub(/^find_by_/, '') => args[0])
676
+ elsif [].respond_to? method
525
677
  all.send(method, *args, &block)
526
678
  elsif ScopeDescription.find(@target_klass, method)
527
679
  apply_scope(method, *args)
528
- elsif args.count == 1 && method.start_with?('find_by_')
529
- apply_scope(:find_by, method.sub(/^find_by_/, '') => args.first)
530
- elsif @target_klass.respond_to?(method) && ScopeDescription.find(@target_klass, "_#{method}")
680
+ elsif !@target_klass.respond_to?(method)
681
+ super
682
+ elsif ScopeDescription.find(@target_klass, "_#{method}")
531
683
  apply_scope("_#{method}", *args).first
532
684
  else
533
- super
685
+ # create a subclass of the original target class that responds to all
686
+ # by returning our collection back
687
+ fake_class = Class.new(@target_klass)
688
+ fake_class.instance_variable_set("@all", self)
689
+ # Opal 0.11 does not handle overridding the original @target_klass
690
+ # with an accessor, so we define the accessor as a method.
691
+ fake_class.define_singleton_method(:all) { @all }
692
+ fake_class.send(method, *args, &block)
534
693
  end
535
694
  end
536
695
 
@@ -552,7 +711,6 @@ To determine this sync_scopes first asks if the record being changed is in the s
552
711
  Hyperstack::Internal::State::Variable.set(self, "collection", collection) unless ReactiveRecord::Base.data_loading?
553
712
  value
554
713
  end
555
-
556
714
  end
557
715
 
558
716
  end