hyper-model 1.0.alpha1.2 → 1.0.alpha1.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +4 -1
- data/.rspec +0 -1
- data/Gemfile +6 -5
- data/Rakefile +27 -3
- data/hyper-model.gemspec +11 -19
- data/lib/active_record_base.rb +105 -33
- 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 +130 -34
- data/lib/reactive_record/active_record/base.rb +32 -0
- data/lib/reactive_record/active_record/class_methods.rb +124 -52
- 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 +226 -68
- 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 +81 -51
- data/lib/reactive_record/active_record/reactive_record/lookup_tables.rb +5 -5
- data/lib/reactive_record/active_record/reactive_record/operations.rb +10 -3
- data/lib/reactive_record/active_record/reactive_record/scoped_collection.rb +3 -0
- data/lib/reactive_record/active_record/reactive_record/setters.rb +105 -68
- data/lib/reactive_record/active_record/reactive_record/while_loading.rb +249 -32
- data/lib/reactive_record/broadcast.rb +62 -25
- data/lib/reactive_record/interval.rb +3 -3
- data/lib/reactive_record/permissions.rb +14 -2
- data/lib/reactive_record/scope_description.rb +3 -2
- data/lib/reactive_record/server_data_cache.rb +99 -49
- data/polymorph-notes.md +143 -0
- data/spec_fails.txt +3 -0
- metadata +54 -153
- data/Gemfile.lock +0 -421
@@ -6,39 +6,38 @@ module ActiveRecord
|
|
6
6
|
# adds method to get the HyperMesh public column types
|
7
7
|
# this works because the public folder is currently required to be eager loaded.
|
8
8
|
class Base
|
9
|
+
@@hyper_stack_public_columns_hash_mutex = Mutex.new
|
9
10
|
def self.public_columns_hash
|
10
|
-
|
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
|
@@ -52,14 +52,19 @@ module ReactiveRecord
|
|
52
52
|
def all
|
53
53
|
observed
|
54
54
|
@dummy_collection.notify if @dummy_collection
|
55
|
+
# unless false && @collection # this fixes https://github.com/hyperstack-org/hyperstack/issues/82 in very limited cases, and breaks otherthings
|
56
|
+
# sync_collection_with_parent
|
57
|
+
# end
|
55
58
|
unless @collection
|
56
59
|
@collection = []
|
57
60
|
if ids = ReactiveRecord::Base.fetch_from_db([*@vector, "*all"])
|
58
61
|
ids.each do |id|
|
59
|
-
@collection <<
|
62
|
+
@collection << ReactiveRecord::Base.find_by_id(@target_klass, id)
|
60
63
|
end
|
61
64
|
else
|
62
65
|
@dummy_collection = ReactiveRecord::Base.load_from_db(nil, *@vector, "*all")
|
66
|
+
# this calls back to all now that the collection is initialized,
|
67
|
+
# so it has the side effect of creating a dummy value in collection[0]
|
63
68
|
@dummy_record = self[0]
|
64
69
|
end
|
65
70
|
end
|
@@ -72,6 +77,8 @@ module ReactiveRecord
|
|
72
77
|
(@collection.length..index).each do |i|
|
73
78
|
new_dummy_record = ReactiveRecord::Base.new_from_vector(@target_klass, nil, *@vector, "*#{i}")
|
74
79
|
new_dummy_record.attributes[@association.inverse_of] = @owner if @association && !@association.through_association?
|
80
|
+
# HMT-TODO: the above needs to be looked into... if we are a hmt then don't we need to create a dummy on the joins collection as well?
|
81
|
+
# or maybe this just does not work for HMT?
|
75
82
|
@collection << new_dummy_record
|
76
83
|
end
|
77
84
|
end
|
@@ -80,8 +87,15 @@ module ReactiveRecord
|
|
80
87
|
|
81
88
|
def ==(other_collection)
|
82
89
|
observed
|
83
|
-
|
90
|
+
# handle special case of other_collection NOT being a collection (typically nil)
|
91
|
+
return (@collection || []) == other_collection unless other_collection.is_a? Collection
|
84
92
|
other_collection.observed
|
93
|
+
# if either collection has not been created then compare the vectors
|
94
|
+
# https://github.com/hyperstack-org/hyperstack/issues/81
|
95
|
+
# TODO: if this works then remove the || [] below (2 of them)
|
96
|
+
if !@collection || !other_collection.collection
|
97
|
+
return @vector == other_collection.vector && unsaved_children == other_collection.unsaved_children
|
98
|
+
end
|
85
99
|
my_children = (@collection || []).select { |target| target != @dummy_record }
|
86
100
|
if other_collection
|
87
101
|
other_children = (other_collection.collection || []).select { |target| target != other_collection.dummy_record }
|
@@ -93,7 +107,7 @@ module ReactiveRecord
|
|
93
107
|
end
|
94
108
|
# todo move following to a separate module related to scope updates ******************
|
95
109
|
attr_reader :vector
|
96
|
-
|
110
|
+
attr_accessor :scope_description
|
97
111
|
attr_writer :parent
|
98
112
|
attr_reader :pre_sync_related_records
|
99
113
|
|
@@ -127,7 +141,10 @@ To determine this sync_scopes first asks if the record being changed is in the s
|
|
127
141
|
|
128
142
|
|
129
143
|
=end
|
144
|
+
attr_accessor :broadcast_updated_at
|
145
|
+
|
130
146
|
def sync_scopes(broadcast)
|
147
|
+
self.broadcast_updated_at = broadcast.updated_at
|
131
148
|
# record_with_current_values will return nil if data between
|
132
149
|
# the broadcast record and the value on the client is out of sync
|
133
150
|
# not running set_pre_sync_related_records will cause sync scopes
|
@@ -145,6 +162,8 @@ To determine this sync_scopes first asks if the record being changed is in the s
|
|
145
162
|
)
|
146
163
|
record.backing_record.sync_unscoped_collection! if record.destroyed? || broadcast.new?
|
147
164
|
end
|
165
|
+
ensure
|
166
|
+
self.broadcast_updated_at = nil
|
148
167
|
end
|
149
168
|
|
150
169
|
def apply_to_all_collections(method, record, dont_gather)
|
@@ -200,7 +219,7 @@ To determine this sync_scopes first asks if the record being changed is in the s
|
|
200
219
|
return [] unless attrs[@association.inverse_of] == @owner
|
201
220
|
if !@association.through_association
|
202
221
|
[record]
|
203
|
-
elsif (source = attrs[@association.source])
|
222
|
+
elsif (source = attrs[@association.source]) && source.is_a?(@target_klass)
|
204
223
|
[source]
|
205
224
|
else
|
206
225
|
[]
|
@@ -212,7 +231,6 @@ To determine this sync_scopes first asks if the record being changed is in the s
|
|
212
231
|
end
|
213
232
|
|
214
233
|
def filter_records(related_records)
|
215
|
-
# possibly we should never get here???
|
216
234
|
scope_args = @vector.last.is_a?(Array) ? @vector.last[1..-1] : []
|
217
235
|
@scope_description.filter_records(related_records, scope_args)
|
218
236
|
end
|
@@ -221,16 +239,23 @@ To determine this sync_scopes first asks if the record being changed is in the s
|
|
221
239
|
@live_scopes ||= Set.new
|
222
240
|
end
|
223
241
|
|
242
|
+
def in_this_collection(related_records)
|
243
|
+
# HMT-TODO: I don't think we can get a set of related records here with a through association unless they are part of the collection
|
244
|
+
return related_records if !@association || @association.through_association?
|
245
|
+
related_records.select do |r|
|
246
|
+
r.backing_record.attributes[@association.inverse_of] == @owner
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
224
250
|
def set_pre_sync_related_records(related_records, _record = nil)
|
225
|
-
|
226
|
-
@pre_sync_related_records = related_records #in_this_collection related_records <- not sure if this works
|
251
|
+
@pre_sync_related_records = in_this_collection(related_records)
|
227
252
|
live_scopes.each { |scope| scope.set_pre_sync_related_records(@pre_sync_related_records) }
|
228
253
|
end
|
229
254
|
|
230
255
|
# NOTE sync_scopes is overridden in scope_description.rb
|
231
256
|
def sync_scopes(related_records, record, filtering = true)
|
232
257
|
#related_records = related_records.intersection([*@collection])
|
233
|
-
|
258
|
+
related_records = in_this_collection(related_records) if filtering
|
234
259
|
live_scopes.each { |scope| scope.sync_scopes(related_records, record, filtering) }
|
235
260
|
notify_of_change unless related_records.empty?
|
236
261
|
ensure
|
@@ -261,11 +286,15 @@ To determine this sync_scopes first asks if the record being changed is in the s
|
|
261
286
|
end
|
262
287
|
|
263
288
|
def link_to_parent
|
264
|
-
|
289
|
+
# puts "#{self}.link_to_parent @linked = #{!!@linked}, collection? #{!!@collection}"
|
290
|
+
# always check that parent is synced - fixes issue https://github.com/hyperstack-org/hyperstack/issues/82
|
291
|
+
# note that sync_collection_with_parent checks to make sure that is NOT a collection and that there IS a parent
|
292
|
+
|
293
|
+
return sync_collection_with_parent if @linked
|
265
294
|
@linked = true
|
266
295
|
if @parent
|
267
296
|
@parent.link_child self
|
268
|
-
sync_collection_with_parent
|
297
|
+
sync_collection_with_parent
|
269
298
|
else
|
270
299
|
ReactiveRecord::Base.add_to_outer_scopes self
|
271
300
|
end
|
@@ -278,14 +307,23 @@ To determine this sync_scopes first asks if the record being changed is in the s
|
|
278
307
|
end
|
279
308
|
|
280
309
|
def sync_collection_with_parent
|
310
|
+
# puts "#{self}.sync_collection_with_parent"
|
311
|
+
return if @collection || !@parent || @parent.dummy_collection # fixes issue https://github.com/hyperstack-org/hyperstack/issues/78 and supports /82
|
281
312
|
if @parent.collection
|
313
|
+
# puts ">>> @parent.collection present"
|
282
314
|
if @parent.collection.empty?
|
315
|
+
# puts ">>>>> @parent.collection is empty!"
|
283
316
|
@collection = []
|
284
317
|
elsif filter?
|
285
|
-
@
|
318
|
+
# puts "#{self}.sync_collection_with_parent (@parent = #{@parent}) calling filter records on (#{@parent.collection})"
|
319
|
+
@collection = filter_records(@parent.collection).to_a
|
286
320
|
end
|
287
|
-
elsif @parent._count_internal(false).zero?
|
321
|
+
elsif !@linked && @parent._count_internal(false).zero?
|
322
|
+
# don't check _count_internal if already linked as this cause an unnecessary rendering cycle
|
323
|
+
# puts ">>> @parent._count_internal(false).zero? is true!"
|
288
324
|
@count = 0
|
325
|
+
else
|
326
|
+
# puts ">>> NOP"
|
289
327
|
end
|
290
328
|
end
|
291
329
|
|
@@ -303,31 +341,33 @@ To determine this sync_scopes first asks if the record being changed is in the s
|
|
303
341
|
end
|
304
342
|
|
305
343
|
def observed
|
306
|
-
return if @observing || ReactiveRecord::Base.data_loading?
|
344
|
+
return self if @observing || ReactiveRecord::Base.data_loading?
|
307
345
|
begin
|
308
346
|
@observing = true
|
309
347
|
link_to_parent
|
310
348
|
reload_from_db(true) if @out_of_date
|
311
349
|
Hyperstack::Internal::State::Variable.get(self, :collection)
|
350
|
+
self
|
312
351
|
ensure
|
313
352
|
@observing = false
|
314
353
|
end
|
315
354
|
end
|
316
355
|
|
317
|
-
def
|
356
|
+
def count_state=(val)
|
318
357
|
unless ReactiveRecord::WhileLoading.observed?
|
319
358
|
Hyperstack::Internal::State::Variable.set(self, :collection, collection, true)
|
320
359
|
end
|
360
|
+
@count_updated_at = ReactiveRecord::Operations::Base.last_response_sent_at
|
321
361
|
@count = val
|
322
362
|
end
|
323
363
|
|
324
|
-
|
325
|
-
|
326
364
|
def _count_internal(load_from_client)
|
327
365
|
# when count is called on a leaf, count_internal is called for each
|
328
366
|
# ancestor. Only the outermost count has load_from_client == true
|
329
367
|
observed
|
330
|
-
if @
|
368
|
+
if @count && @dummy_collection
|
369
|
+
@count # fixes https://github.com/hyperstack-org/hyperstack/issues/79
|
370
|
+
elsif @collection
|
331
371
|
@collection.count
|
332
372
|
elsif @count ||= ReactiveRecord::Base.fetch_from_db([*@vector, "*count"])
|
333
373
|
@count
|
@@ -366,7 +406,7 @@ To determine this sync_scopes first asks if the record being changed is in the s
|
|
366
406
|
# child.test_model = 1
|
367
407
|
# so... we go back starting at this collection and look for the first
|
368
408
|
# collection with an owner... that is our guy
|
369
|
-
child = proxy_association.klass
|
409
|
+
child = ReactiveRecord::Base.find_by_id(proxy_association.klass, id)
|
370
410
|
push child
|
371
411
|
set_belongs_to child
|
372
412
|
end
|
@@ -374,7 +414,11 @@ To determine this sync_scopes first asks if the record being changed is in the s
|
|
374
414
|
def set_belongs_to(child)
|
375
415
|
if @owner
|
376
416
|
# TODO this is major broken...current
|
377
|
-
|
417
|
+
if (through_association = @association.through_association)
|
418
|
+
# HMT-TODO: create a new record with owner and child
|
419
|
+
else
|
420
|
+
child.send("#{@association.inverse_of}=", @owner) if @association && !@association.through_association
|
421
|
+
end
|
378
422
|
elsif @parent
|
379
423
|
@parent.set_belongs_to(child)
|
380
424
|
end
|
@@ -389,36 +433,57 @@ To determine this sync_scopes first asks if the record being changed is in the s
|
|
389
433
|
|
390
434
|
def update_child(item)
|
391
435
|
backing_record = item.backing_record
|
392
|
-
|
436
|
+
# HMT TODO: The following && !association.through_association was commented out, causing wrong class items to be added to
|
437
|
+
# associations
|
438
|
+
# Why was it commented out.
|
439
|
+
if backing_record && @owner && @association && item.attributes[@association.inverse_of] != @owner && !@association.through_association?
|
393
440
|
inverse_of = @association.inverse_of
|
394
|
-
|
441
|
+
current_association_value = item.attributes[inverse_of]
|
395
442
|
backing_record.virgin = false unless backing_record.data_loading?
|
443
|
+
# next line was commented out and following line was active.
|
396
444
|
backing_record.update_belongs_to(inverse_of, @owner)
|
397
|
-
|
398
|
-
|
399
|
-
|
445
|
+
#backing_record.set_belongs_to_via_has_many(@association, @owner)
|
446
|
+
# following is handled by update_belongs_to and is redundant
|
447
|
+
# unless current_association_value.nil? # might be a dummy value which responds to nil
|
448
|
+
# current_association = @association.inverse.inverse(current_association_value)
|
449
|
+
# current_association_attribute = current_association.attribute
|
450
|
+
# if current_association.collection? && current_association_value.attributes[current_association_attribute]
|
451
|
+
# current_association.attributes[current_association_attribute].delete(item)
|
452
|
+
# end
|
453
|
+
# end
|
400
454
|
@owner.backing_record.sync_has_many(@association.attribute)
|
401
455
|
end
|
402
456
|
end
|
403
457
|
|
404
458
|
def push(item)
|
405
|
-
|
406
|
-
|
407
|
-
self
|
459
|
+
if (through_association = @association&.through_association)
|
460
|
+
through_association.klass.create(@association.inverse_of => @owner, @association.source => item)
|
461
|
+
self
|
408
462
|
else
|
409
|
-
|
410
|
-
update_child(item)
|
411
|
-
@owner.backing_record.sync_has_many(@association.attribute) if @owner && @association
|
412
|
-
if !@count.nil?
|
413
|
-
@count += item.destroyed? ? -1 : 1
|
414
|
-
notify_of_change self
|
415
|
-
end
|
463
|
+
_internal_push(item)
|
416
464
|
end
|
417
|
-
self
|
418
465
|
end
|
419
466
|
|
420
467
|
alias << push
|
421
468
|
|
469
|
+
def _internal_push(item)
|
470
|
+
insure_sync do
|
471
|
+
item.itself # force get of at least the id
|
472
|
+
if collection
|
473
|
+
self.force_push item
|
474
|
+
else
|
475
|
+
unsaved_children << item
|
476
|
+
update_child(item)
|
477
|
+
@owner.backing_record.sync_has_many(@association.attribute) if @owner && @association
|
478
|
+
if !@count.nil?
|
479
|
+
@count += (item.destroyed? ? -1 : 1)
|
480
|
+
notify_of_change self
|
481
|
+
end
|
482
|
+
end
|
483
|
+
end
|
484
|
+
self
|
485
|
+
end
|
486
|
+
|
422
487
|
def sort!(*args, &block)
|
423
488
|
replace(sort(*args, &block))
|
424
489
|
end
|
@@ -438,13 +503,29 @@ To determine this sync_scopes first asks if the record being changed is in the s
|
|
438
503
|
notify_of_change self
|
439
504
|
end
|
440
505
|
|
441
|
-
[:first, :last].each do |method|
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
506
|
+
# [:first, :last].each do |method|
|
507
|
+
# define_method method do |*args|
|
508
|
+
# if args.count == 0
|
509
|
+
# all.send(method)
|
510
|
+
# else
|
511
|
+
# apply_scope(method, *args)
|
512
|
+
# end
|
513
|
+
# end
|
514
|
+
# end
|
515
|
+
|
516
|
+
def first(n = nil)
|
517
|
+
if n
|
518
|
+
apply_scope(:first, n)
|
519
|
+
else
|
520
|
+
self[0]
|
521
|
+
end
|
522
|
+
end
|
523
|
+
|
524
|
+
def last(n = nil)
|
525
|
+
if n
|
526
|
+
apply_scope(:__hyperstack_internal_scoped_last_n, n)
|
527
|
+
else
|
528
|
+
__hyperstack_internal_scoped_last
|
448
529
|
end
|
449
530
|
end
|
450
531
|
|
@@ -466,46 +547,73 @@ To determine this sync_scopes first asks if the record being changed is in the s
|
|
466
547
|
@dummy_collection.notify
|
467
548
|
array = new_array.is_a?(Collection) ? new_array.collection : new_array
|
468
549
|
@collection.each_with_index do |r, i|
|
469
|
-
r.id = new_array[i].id if array[i] and array[i].id and !r.
|
550
|
+
r.id = new_array[i].id if array[i] and array[i].id and !r.new_record? and r.backing_record.vector.last =~ /^\*[0-9]+$/
|
470
551
|
end
|
471
552
|
end
|
472
|
-
|
473
|
-
@collection.dup.each { |item| delete(item) } if @collection
|
553
|
+
# the following makes sure that the existing elements are properly removed from the collection
|
554
|
+
@collection.dup.each { |item| delete(item) } if @collection
|
474
555
|
@collection = []
|
475
556
|
if new_array.is_a? Collection
|
476
557
|
@dummy_collection = new_array.dummy_collection
|
477
558
|
@dummy_record = new_array.dummy_record
|
478
|
-
new_array.collection.each { |item|
|
559
|
+
new_array.collection.each { |item| _internal_push item } if new_array.collection
|
479
560
|
else
|
480
561
|
@dummy_collection = @dummy_record = nil
|
481
|
-
new_array.each { |item|
|
562
|
+
new_array.each { |item| _internal_push item }
|
482
563
|
end
|
483
564
|
notify_of_change new_array
|
484
565
|
end
|
485
566
|
|
486
|
-
def
|
487
|
-
|
488
|
-
|
489
|
-
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
|
490
571
|
inverse_of = @association.inverse_of
|
491
|
-
if (backing_record = item.backing_record) && item.attributes[inverse_of] == @owner
|
572
|
+
if (backing_record = item.backing_record) && item.attributes[inverse_of] == @owner && !@association.through_association?
|
492
573
|
# the if prevents double update if delete is being called from << (see << above)
|
493
574
|
backing_record.update_belongs_to(inverse_of, nil)
|
494
575
|
end
|
495
576
|
delete_internal(item) { @owner.backing_record.sync_has_many(@association.attribute) }
|
496
577
|
else
|
497
578
|
delete_internal(item)
|
498
|
-
end
|
579
|
+
end.tap { Hyperstack::Internal::State::Variable.set(self, :collection, collection) }
|
580
|
+
end
|
581
|
+
end
|
582
|
+
|
583
|
+
def destroy(item)
|
584
|
+
return destroy_non_habtm(item) unless @association&.habtm?
|
585
|
+
|
586
|
+
ta = @association.through_association
|
587
|
+
item_foreign_key = @association.source_belongs_to_association.association_foreign_key
|
588
|
+
join_record = ta.klass.find_by(
|
589
|
+
ta.association_foreign_key => @owner.id,
|
590
|
+
item_foreign_key => item.id
|
499
591
|
)
|
592
|
+
return destroy_non_habtm(item) if join_record.nil? ||
|
593
|
+
join_record.backing_record.being_destroyed
|
594
|
+
|
595
|
+
join_record&.destroy
|
500
596
|
end
|
501
597
|
|
598
|
+
def insure_sync
|
599
|
+
if Collection.broadcast_updated_at && @count_updated_at && Collection.broadcast_updated_at < @count_updated_at
|
600
|
+
reload_from_db
|
601
|
+
else
|
602
|
+
yield
|
603
|
+
end
|
604
|
+
end
|
605
|
+
|
606
|
+
alias delete destroy
|
607
|
+
|
502
608
|
def delete_internal(item)
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
609
|
+
insure_sync do
|
610
|
+
if collection
|
611
|
+
all.delete(item)
|
612
|
+
elsif !@count.nil?
|
613
|
+
@count -= 1
|
614
|
+
end
|
615
|
+
yield if block_given? # was yield item, but item is not used
|
507
616
|
end
|
508
|
-
yield if block_given? # was yield item, but item is not used
|
509
617
|
item
|
510
618
|
end
|
511
619
|
|
@@ -514,23 +622,74 @@ To determine this sync_scopes first asks if the record being changed is in the s
|
|
514
622
|
@dummy_collection.loading?
|
515
623
|
end
|
516
624
|
|
625
|
+
def find_by(attrs)
|
626
|
+
attrs = @target_klass.__hyperstack_preprocess_attrs(attrs)
|
627
|
+
(r = __hyperstack_internal_scoped_find_by(attrs)) || return
|
628
|
+
r.backing_record.sync_attributes(attrs).set_ar_instance!
|
629
|
+
end
|
630
|
+
|
631
|
+
def find(*args)
|
632
|
+
args = args[0] if args[0].is_a? Array
|
633
|
+
return args.collect { |id| find(id) } if args.count > 1
|
634
|
+
find_by(@target_klass.primary_key => args[0])
|
635
|
+
end
|
636
|
+
|
637
|
+
def _find_by_initializer(scope, attrs)
|
638
|
+
found =
|
639
|
+
if scope.is_a? Collection
|
640
|
+
scope.parent.collection&.detect { |lr| !attrs.detect { |k, v| lr.attributes[k] != v } }
|
641
|
+
else
|
642
|
+
ReactiveRecord::Base.find_locally(@target_klass, attrs)&.ar_instance
|
643
|
+
end
|
644
|
+
return first unless found
|
645
|
+
@collection = [found]
|
646
|
+
found
|
647
|
+
end
|
648
|
+
|
649
|
+
# to avoid fetching the entire collection array we check empty and any against the count
|
650
|
+
|
517
651
|
def empty?
|
518
|
-
|
519
|
-
|
520
|
-
|
652
|
+
count.zero?
|
653
|
+
end
|
654
|
+
|
655
|
+
def any?(*args, &block)
|
656
|
+
# If there are any args passed in, then the collection is being used in the condition
|
657
|
+
# and we must load it all into memory.
|
658
|
+
return all.any?(*args, &block) if args&.length&.positive? || block.present?
|
659
|
+
|
660
|
+
# Otherwise we can just check the count for efficiency
|
661
|
+
!empty?
|
662
|
+
end
|
663
|
+
|
664
|
+
def none?(*args, &block)
|
665
|
+
# If there are any args passed in, then the collection is being used in the condition
|
666
|
+
# and we must load it all into memory.
|
667
|
+
return all.none?(*args, &block) if args&.length&.positive? || block.present?
|
668
|
+
|
669
|
+
# Otherwise we can just check the count for efficiency
|
670
|
+
empty?
|
521
671
|
end
|
522
672
|
|
523
673
|
def method_missing(method, *args, &block)
|
524
|
-
if
|
674
|
+
if args.count == 1 && method.start_with?('find_by_')
|
675
|
+
find_by(method.sub(/^find_by_/, '') => args[0])
|
676
|
+
elsif [].respond_to? method
|
525
677
|
all.send(method, *args, &block)
|
526
678
|
elsif ScopeDescription.find(@target_klass, method)
|
527
679
|
apply_scope(method, *args)
|
528
|
-
elsif
|
529
|
-
|
530
|
-
elsif
|
680
|
+
elsif !@target_klass.respond_to?(method)
|
681
|
+
super
|
682
|
+
elsif ScopeDescription.find(@target_klass, "_#{method}")
|
531
683
|
apply_scope("_#{method}", *args).first
|
532
684
|
else
|
533
|
-
|
685
|
+
# create a subclass of the original target class that responds to all
|
686
|
+
# by returning our collection back
|
687
|
+
fake_class = Class.new(@target_klass)
|
688
|
+
fake_class.instance_variable_set("@all", self)
|
689
|
+
# Opal 0.11 does not handle overridding the original @target_klass
|
690
|
+
# with an accessor, so we define the accessor as a method.
|
691
|
+
fake_class.define_singleton_method(:all) { @all }
|
692
|
+
fake_class.send(method, *args, &block)
|
534
693
|
end
|
535
694
|
end
|
536
695
|
|
@@ -552,7 +711,6 @@ To determine this sync_scopes first asks if the record being changed is in the s
|
|
552
711
|
Hyperstack::Internal::State::Variable.set(self, "collection", collection) unless ReactiveRecord::Base.data_loading?
|
553
712
|
value
|
554
713
|
end
|
555
|
-
|
556
714
|
end
|
557
715
|
|
558
716
|
end
|