hyper-model 1.0.alpha1.3 → 1.0.alpha1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|