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.
- checksums.yaml +4 -4
- data/.gitignore +4 -1
- data/.rspec +0 -1
- data/Gemfile +6 -5
- data/Rakefile +18 -6
- data/hyper-model.gemspec +12 -20
- data/lib/active_record_base.rb +95 -28
- data/lib/enumerable/pluck.rb +3 -2
- data/lib/hyper-model.rb +4 -1
- data/lib/hyper_model/version.rb +1 -1
- data/lib/hyper_react/input_tags.rb +2 -1
- data/lib/reactive_record/active_record/associations.rb +125 -35
- data/lib/reactive_record/active_record/base.rb +32 -0
- data/lib/reactive_record/active_record/class_methods.rb +125 -53
- data/lib/reactive_record/active_record/error.rb +2 -0
- data/lib/reactive_record/active_record/errors.rb +8 -4
- data/lib/reactive_record/active_record/instance_methods.rb +73 -5
- data/lib/reactive_record/active_record/public_columns_hash.rb +25 -26
- data/lib/reactive_record/active_record/reactive_record/backing_record_inspector.rb +22 -5
- data/lib/reactive_record/active_record/reactive_record/base.rb +50 -24
- data/lib/reactive_record/active_record/reactive_record/collection.rb +196 -63
- data/lib/reactive_record/active_record/reactive_record/dummy_polymorph.rb +22 -0
- data/lib/reactive_record/active_record/reactive_record/dummy_value.rb +27 -15
- data/lib/reactive_record/active_record/reactive_record/getters.rb +33 -10
- data/lib/reactive_record/active_record/reactive_record/isomorphic_base.rb +71 -44
- data/lib/reactive_record/active_record/reactive_record/lookup_tables.rb +5 -5
- data/lib/reactive_record/active_record/reactive_record/operations.rb +7 -1
- data/lib/reactive_record/active_record/reactive_record/scoped_collection.rb +3 -6
- data/lib/reactive_record/active_record/reactive_record/setters.rb +105 -68
- data/lib/reactive_record/active_record/reactive_record/while_loading.rb +22 -1
- data/lib/reactive_record/broadcast.rb +59 -25
- data/lib/reactive_record/interval.rb +3 -3
- data/lib/reactive_record/permissions.rb +1 -1
- data/lib/reactive_record/scope_description.rb +3 -2
- data/lib/reactive_record/server_data_cache.rb +78 -48
- data/polymorph-notes.md +143 -0
- metadata +52 -157
- 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
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
-
|
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
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
8
|
-
return new_details
|
7
|
+
return error_details unless errors.empty?
|
8
|
+
return new_details if new?
|
9
9
|
return destroyed_details if destroyed
|
10
|
-
return loading_details
|
11
|
-
return dirty_details
|
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 #{
|
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
|
-
|
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
|
-
|
106
|
-
record
|
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(
|
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,
|
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
|
257
|
+
model.unscoped._internal_push ar_instance
|
251
258
|
@synced_with_unscoped = !@synced_with_unscoped
|
252
259
|
end
|
253
260
|
|
254
|
-
def
|
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)
|
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
|
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 <<
|
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
|
-
|
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
|
-
|
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
|
-
|
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)
|
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
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
423
|
-
|
424
|
-
|
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
|
-
|
431
|
-
|
432
|
-
self
|
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
|
-
|
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
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
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.
|
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
|
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|
|
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|
|
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
|
512
|
-
|
513
|
-
|
514
|
-
if @owner && @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
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
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
|
-
|
544
|
-
|
545
|
-
|
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
|
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
|
554
|
-
|
555
|
-
elsif
|
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
|
-
|
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
|