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

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -1
  3. data/.rspec +0 -1
  4. data/Gemfile +6 -5
  5. data/Rakefile +18 -6
  6. data/hyper-model.gemspec +12 -20
  7. data/lib/active_record_base.rb +95 -28
  8. data/lib/enumerable/pluck.rb +3 -2
  9. data/lib/hyper-model.rb +4 -1
  10. data/lib/hyper_model/version.rb +1 -1
  11. data/lib/hyper_react/input_tags.rb +2 -1
  12. data/lib/reactive_record/active_record/associations.rb +125 -35
  13. data/lib/reactive_record/active_record/base.rb +32 -0
  14. data/lib/reactive_record/active_record/class_methods.rb +125 -53
  15. data/lib/reactive_record/active_record/error.rb +2 -0
  16. data/lib/reactive_record/active_record/errors.rb +8 -4
  17. data/lib/reactive_record/active_record/instance_methods.rb +73 -5
  18. data/lib/reactive_record/active_record/public_columns_hash.rb +25 -26
  19. data/lib/reactive_record/active_record/reactive_record/backing_record_inspector.rb +22 -5
  20. data/lib/reactive_record/active_record/reactive_record/base.rb +50 -24
  21. data/lib/reactive_record/active_record/reactive_record/collection.rb +196 -63
  22. data/lib/reactive_record/active_record/reactive_record/dummy_polymorph.rb +22 -0
  23. data/lib/reactive_record/active_record/reactive_record/dummy_value.rb +27 -15
  24. data/lib/reactive_record/active_record/reactive_record/getters.rb +33 -10
  25. data/lib/reactive_record/active_record/reactive_record/isomorphic_base.rb +71 -44
  26. data/lib/reactive_record/active_record/reactive_record/lookup_tables.rb +5 -5
  27. data/lib/reactive_record/active_record/reactive_record/operations.rb +7 -1
  28. data/lib/reactive_record/active_record/reactive_record/scoped_collection.rb +3 -6
  29. data/lib/reactive_record/active_record/reactive_record/setters.rb +105 -68
  30. data/lib/reactive_record/active_record/reactive_record/while_loading.rb +22 -1
  31. data/lib/reactive_record/broadcast.rb +59 -25
  32. data/lib/reactive_record/interval.rb +3 -3
  33. data/lib/reactive_record/permissions.rb +1 -1
  34. data/lib/reactive_record/scope_description.rb +3 -2
  35. data/lib/reactive_record/server_data_cache.rb +78 -48
  36. data/polymorph-notes.md +143 -0
  37. metadata +52 -157
  38. data/Gemfile.lock +0 -440
@@ -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
@@ -59,10 +59,12 @@ module ReactiveRecord
59
59
  @collection = []
60
60
  if ids = ReactiveRecord::Base.fetch_from_db([*@vector, "*all"])
61
61
  ids.each do |id|
62
- @collection << @target_klass.find_by(@target_klass.primary_key => id)
62
+ @collection << ReactiveRecord::Base.find_by_id(@target_klass, id)
63
63
  end
64
64
  else
65
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]
66
68
  @dummy_record = self[0]
67
69
  end
68
70
  end
@@ -75,6 +77,8 @@ module ReactiveRecord
75
77
  (@collection.length..index).each do |i|
76
78
  new_dummy_record = ReactiveRecord::Base.new_from_vector(@target_klass, nil, *@vector, "*#{i}")
77
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?
78
82
  @collection << new_dummy_record
79
83
  end
80
84
  end
@@ -103,7 +107,7 @@ module ReactiveRecord
103
107
  end
104
108
  # todo move following to a separate module related to scope updates ******************
105
109
  attr_reader :vector
106
- attr_writer :scope_description
110
+ attr_accessor :scope_description
107
111
  attr_writer :parent
108
112
  attr_reader :pre_sync_related_records
109
113
 
@@ -137,7 +141,10 @@ To determine this sync_scopes first asks if the record being changed is in the s
137
141
 
138
142
 
139
143
  =end
144
+ attr_accessor :broadcast_updated_at
145
+
140
146
  def sync_scopes(broadcast)
147
+ self.broadcast_updated_at = broadcast.updated_at
141
148
  # record_with_current_values will return nil if data between
142
149
  # the broadcast record and the value on the client is out of sync
143
150
  # not running set_pre_sync_related_records will cause sync scopes
@@ -155,6 +162,8 @@ To determine this sync_scopes first asks if the record being changed is in the s
155
162
  )
156
163
  record.backing_record.sync_unscoped_collection! if record.destroyed? || broadcast.new?
157
164
  end
165
+ ensure
166
+ self.broadcast_updated_at = nil
158
167
  end
159
168
 
160
169
  def apply_to_all_collections(method, record, dont_gather)
@@ -210,7 +219,7 @@ To determine this sync_scopes first asks if the record being changed is in the s
210
219
  return [] unless attrs[@association.inverse_of] == @owner
211
220
  if !@association.through_association
212
221
  [record]
213
- elsif (source = attrs[@association.source])
222
+ elsif (source = attrs[@association.source]) && source.is_a?(@target_klass)
214
223
  [source]
215
224
  else
216
225
  []
@@ -222,7 +231,6 @@ To determine this sync_scopes first asks if the record being changed is in the s
222
231
  end
223
232
 
224
233
  def filter_records(related_records)
225
- # possibly we should never get here???
226
234
  scope_args = @vector.last.is_a?(Array) ? @vector.last[1..-1] : []
227
235
  @scope_description.filter_records(related_records, scope_args)
228
236
  end
@@ -231,16 +239,23 @@ To determine this sync_scopes first asks if the record being changed is in the s
231
239
  @live_scopes ||= Set.new
232
240
  end
233
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
+
234
250
  def set_pre_sync_related_records(related_records, _record = nil)
235
- #related_records = related_records.intersection([*@collection]) <- deleting this works
236
- @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)
237
252
  live_scopes.each { |scope| scope.set_pre_sync_related_records(@pre_sync_related_records) }
238
253
  end
239
254
 
240
255
  # NOTE sync_scopes is overridden in scope_description.rb
241
256
  def sync_scopes(related_records, record, filtering = true)
242
257
  #related_records = related_records.intersection([*@collection])
243
- #related_records = in_this_collection related_records
258
+ related_records = in_this_collection(related_records) if filtering
244
259
  live_scopes.each { |scope| scope.sync_scopes(related_records, record, filtering) }
245
260
  notify_of_change unless related_records.empty?
246
261
  ensure
@@ -301,7 +316,7 @@ To determine this sync_scopes first asks if the record being changed is in the s
301
316
  @collection = []
302
317
  elsif filter?
303
318
  # puts "#{self}.sync_collection_with_parent (@parent = #{@parent}) calling filter records on (#{@parent.collection})"
304
- @collection = filter_records(@parent.collection) # .tap { |rr| puts "returns #{rr} #{rr.to_a}" }
319
+ @collection = filter_records(@parent.collection).to_a
305
320
  end
306
321
  elsif !@linked && @parent._count_internal(false).zero?
307
322
  # don't check _count_internal if already linked as this cause an unnecessary rendering cycle
@@ -326,26 +341,26 @@ To determine this sync_scopes first asks if the record being changed is in the s
326
341
  end
327
342
 
328
343
  def observed
329
- return if @observing || ReactiveRecord::Base.data_loading?
344
+ return self if @observing || ReactiveRecord::Base.data_loading?
330
345
  begin
331
346
  @observing = true
332
347
  link_to_parent
333
348
  reload_from_db(true) if @out_of_date
334
349
  Hyperstack::Internal::State::Variable.get(self, :collection)
350
+ self
335
351
  ensure
336
352
  @observing = false
337
353
  end
338
354
  end
339
355
 
340
- def set_count_state(val)
356
+ def count_state=(val)
341
357
  unless ReactiveRecord::WhileLoading.observed?
342
358
  Hyperstack::Internal::State::Variable.set(self, :collection, collection, true)
343
359
  end
360
+ @count_updated_at = ReactiveRecord::Operations::Base.last_response_sent_at
344
361
  @count = val
345
362
  end
346
363
 
347
-
348
-
349
364
  def _count_internal(load_from_client)
350
365
  # when count is called on a leaf, count_internal is called for each
351
366
  # ancestor. Only the outermost count has load_from_client == true
@@ -391,7 +406,7 @@ To determine this sync_scopes first asks if the record being changed is in the s
391
406
  # child.test_model = 1
392
407
  # so... we go back starting at this collection and look for the first
393
408
  # collection with an owner... that is our guy
394
- child = proxy_association.klass.find(id)
409
+ child = ReactiveRecord::Base.find_by_id(proxy_association.klass, id)
395
410
  push child
396
411
  set_belongs_to child
397
412
  end
@@ -399,7 +414,11 @@ To determine this sync_scopes first asks if the record being changed is in the s
399
414
  def set_belongs_to(child)
400
415
  if @owner
401
416
  # TODO this is major broken...current
402
- 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
403
422
  elsif @parent
404
423
  @parent.set_belongs_to(child)
405
424
  end
@@ -414,36 +433,57 @@ To determine this sync_scopes first asks if the record being changed is in the s
414
433
 
415
434
  def update_child(item)
416
435
  backing_record = item.backing_record
417
- 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?
418
440
  inverse_of = @association.inverse_of
419
- current_association = item.attributes[inverse_of]
441
+ current_association_value = item.attributes[inverse_of]
420
442
  backing_record.virgin = false unless backing_record.data_loading?
443
+ # next line was commented out and following line was active.
421
444
  backing_record.update_belongs_to(inverse_of, @owner)
422
- if current_association && current_association.attributes[@association.attribute]
423
- current_association.attributes[@association.attribute].delete(item)
424
- 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
425
454
  @owner.backing_record.sync_has_many(@association.attribute)
426
455
  end
427
456
  end
428
457
 
429
458
  def push(item)
430
- item.itself # force get of at least the id
431
- if collection
432
- 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
433
462
  else
434
- unsaved_children << item
435
- update_child(item)
436
- @owner.backing_record.sync_has_many(@association.attribute) if @owner && @association
437
- if !@count.nil?
438
- @count += item.destroyed? ? -1 : 1
439
- notify_of_change self
440
- end
463
+ _internal_push(item)
441
464
  end
442
- self
443
465
  end
444
466
 
445
467
  alias << push
446
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
+
447
487
  def sort!(*args, &block)
448
488
  replace(sort(*args, &block))
449
489
  end
@@ -463,13 +503,29 @@ To determine this sync_scopes first asks if the record being changed is in the s
463
503
  notify_of_change self
464
504
  end
465
505
 
466
- [:first, :last].each do |method|
467
- define_method method do |*args|
468
- if args.count == 0
469
- all.send(method)
470
- else
471
- apply_scope(method, *args)
472
- 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
473
529
  end
474
530
  end
475
531
 
@@ -491,46 +547,73 @@ To determine this sync_scopes first asks if the record being changed is in the s
491
547
  @dummy_collection.notify
492
548
  array = new_array.is_a?(Collection) ? new_array.collection : new_array
493
549
  @collection.each_with_index do |r, i|
494
- 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]+$/
495
551
  end
496
552
  end
497
-
498
- @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
499
555
  @collection = []
500
556
  if new_array.is_a? Collection
501
557
  @dummy_collection = new_array.dummy_collection
502
558
  @dummy_record = new_array.dummy_record
503
- new_array.collection.each { |item| self << item } if new_array.collection
559
+ new_array.collection.each { |item| _internal_push item } if new_array.collection
504
560
  else
505
561
  @dummy_collection = @dummy_record = nil
506
- new_array.each { |item| self << item }
562
+ new_array.each { |item| _internal_push item }
507
563
  end
508
564
  notify_of_change new_array
509
565
  end
510
566
 
511
- def delete(item)
512
- unsaved_children.delete(item)
513
- notify_of_change(
514
- 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
515
571
  inverse_of = @association.inverse_of
516
- 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?
517
573
  # the if prevents double update if delete is being called from << (see << above)
518
574
  backing_record.update_belongs_to(inverse_of, nil)
519
575
  end
520
576
  delete_internal(item) { @owner.backing_record.sync_has_many(@association.attribute) }
521
577
  else
522
578
  delete_internal(item)
523
- 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
524
591
  )
592
+ return destroy_non_habtm(item) if join_record.nil? ||
593
+ join_record.backing_record.being_destroyed
594
+
595
+ join_record&.destroy
525
596
  end
526
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
+
527
608
  def delete_internal(item)
528
- if collection
529
- all.delete(item)
530
- elsif !@count.nil?
531
- @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
532
616
  end
533
- yield if block_given? # was yield item, but item is not used
534
617
  item
535
618
  end
536
619
 
@@ -539,23 +622,74 @@ To determine this sync_scopes first asks if the record being changed is in the s
539
622
  @dummy_collection.loading?
540
623
  end
541
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
+
542
651
  def empty?
543
- # should be handled by method missing below, but opal-rspec does not deal well
544
- # with method missing, so to test...
545
- 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?
546
671
  end
547
672
 
548
673
  def method_missing(method, *args, &block)
549
- 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
550
677
  all.send(method, *args, &block)
551
678
  elsif ScopeDescription.find(@target_klass, method)
552
679
  apply_scope(method, *args)
553
- elsif args.count == 1 && method.start_with?('find_by_')
554
- apply_scope(:find_by, method.sub(/^find_by_/, '') => args.first)
555
- 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}")
556
683
  apply_scope("_#{method}", *args).first
557
684
  else
558
- 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)
559
693
  end
560
694
  end
561
695
 
@@ -577,7 +711,6 @@ To determine this sync_scopes first asks if the record being changed is in the s
577
711
  Hyperstack::Internal::State::Variable.set(self, "collection", collection) unless ReactiveRecord::Base.data_loading?
578
712
  value
579
713
  end
580
-
581
714
  end
582
715
 
583
716
  end