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
@@ -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,15 +181,16 @@ 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
 
181
188
  def changed?(*args)
182
189
  if args.count == 0
183
- Hyperstack::Internal::Store::State.get_state(self, "!CHANGED!")
190
+ Hyperstack::Internal::State::Variable.get(self, "!CHANGED!")
184
191
  !changed_attributes.empty?
185
192
  else
186
- Hyperstack::Internal::Store::State.get_state(self, args[0])
193
+ Hyperstack::Internal::State::Variable.get(self, args[0])
187
194
  changed_attributes.include? args[0]
188
195
  end
189
196
  end
@@ -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])
@@ -288,13 +307,15 @@ module ReactiveRecord
288
307
  end
289
308
 
290
309
  def saving!
291
- Hyperstack::Internal::Store::State.set_state(self, self, :saving) unless data_loading?
310
+ Hyperstack::Internal::State::Variable.set(self, self, :saving) unless data_loading?
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,13 +323,17 @@ 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
308
333
  if errors.empty?
309
- Hyperstack::Internal::Store::State.set_state(self, self, :saved)
334
+ Hyperstack::Internal::State::Variable.set(self, self, :saved)
310
335
  elsif !data_loading?
311
- Hyperstack::Internal::Store::State.set_state(self, self, :error)
336
+ Hyperstack::Internal::State::Variable.set(self, self, :error)
312
337
  end
313
338
  self
314
339
  end
@@ -334,7 +359,7 @@ module ReactiveRecord
334
359
  end
335
360
 
336
361
  def saving?
337
- Hyperstack::Internal::Store::State.get_state(self, self)
362
+ Hyperstack::Internal::State::Variable.get(self, self)
338
363
  @saving
339
364
  end
340
365
 
@@ -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,21 +307,30 @@ 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
 
292
330
  # end of stuff to move
293
331
 
294
332
  def reload_from_db(force = nil)
295
- if force || Hyperstack::Internal::Store::State.observed?(self, :collection)
333
+ if force || Hyperstack::Internal::State::Variable.observed?(self, :collection)
296
334
  @out_of_date = false
297
335
  ReactiveRecord::Base.load_from_db(nil, *@vector, '*all') if @collection
298
336
  ReactiveRecord::Base.load_from_db(nil, *@vector, '*count')
@@ -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
- Hyperstack::Internal::Store::State.get_state(self, :collection)
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
- Hyperstack::Internal::Store::State.set_state(self, :collection, collection, true)
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
596
+ end
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
500
604
  end
501
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,19 +622,61 @@ 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
680
  elsif @target_klass.respond_to?(method) && ScopeDescription.find(@target_klass, "_#{method}")
531
681
  apply_scope("_#{method}", *args).first
532
682
  else
@@ -549,10 +699,9 @@ To determine this sync_scopes first asks if the record being changed is in the s
549
699
  end
550
700
 
551
701
  def notify_of_change(value = nil)
552
- Hyperstack::Internal::Store::State.set_state(self, "collection", collection) unless ReactiveRecord::Base.data_loading?
702
+ Hyperstack::Internal::State::Variable.set(self, "collection", collection) unless ReactiveRecord::Base.data_loading?
553
703
  value
554
704
  end
555
-
556
705
  end
557
706
 
558
707
  end