hyper-model 0.6.0 → 0.99.0
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 +5 -5
- data/.gitignore +35 -41
- data/.rspec +2 -0
- data/.travis.yml +33 -0
- data/CHANGELOG.md +34 -0
- data/DOCS.md +735 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +298 -224
- data/{LICENSE → LICENSE.txt} +6 -6
- data/README.md +51 -2
- data/Rakefile +18 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/codeship.database.yml +18 -0
- data/hyper-model.gemspec +62 -36
- data/lib/active_model_client_stubs.rb +16 -0
- data/lib/active_record_base.rb +331 -0
- data/{examples/chat-app/app/assets/images/.keep → lib/acts_as_string.rb} +0 -0
- data/lib/enumerable/pluck.rb +6 -0
- data/lib/hyper-model.rb +59 -8
- data/lib/hyper_model/version.rb +3 -0
- data/lib/hyper_react/input_tags.rb +47 -0
- data/lib/hyperloop/model/load.rb +1 -1
- data/lib/kernel/itself.rb +5 -0
- data/lib/object/tap.rb +7 -0
- data/lib/opal/equality_patches.rb +15 -0
- data/lib/opal/parse_patch.rb +14 -0
- data/lib/opal/set_patches.rb +8 -0
- data/lib/reactive_record/active_record/aggregations.rb +69 -0
- data/lib/reactive_record/active_record/associations.rb +118 -0
- data/lib/reactive_record/active_record/base.rb +10 -0
- data/lib/reactive_record/active_record/class_methods.rb +406 -0
- data/lib/reactive_record/active_record/error.rb +31 -0
- data/lib/reactive_record/active_record/errors.rb +374 -0
- data/lib/reactive_record/active_record/instance_methods.rb +187 -0
- data/lib/reactive_record/active_record/public_columns_hash.rb +44 -0
- data/lib/reactive_record/active_record/reactive_record/backing_record_inspector.rb +36 -0
- data/lib/reactive_record/active_record/reactive_record/base.rb +416 -0
- data/lib/reactive_record/active_record/reactive_record/collection.rb +558 -0
- data/lib/reactive_record/active_record/reactive_record/column_types.rb +75 -0
- data/lib/reactive_record/active_record/reactive_record/dummy_value.rb +236 -0
- data/lib/reactive_record/active_record/reactive_record/getters.rb +133 -0
- data/lib/reactive_record/active_record/reactive_record/isomorphic_base.rb +576 -0
- data/lib/reactive_record/active_record/reactive_record/lookup_tables.rb +54 -0
- data/lib/reactive_record/active_record/reactive_record/operations.rb +107 -0
- data/lib/reactive_record/active_record/reactive_record/scoped_collection.rb +62 -0
- data/lib/reactive_record/active_record/reactive_record/setters.rb +194 -0
- data/lib/reactive_record/active_record/reactive_record/unscoped_collection.rb +16 -0
- data/lib/reactive_record/active_record/reactive_record/while_loading.rb +343 -0
- data/lib/reactive_record/active_record_error.rb +4 -0
- data/lib/reactive_record/broadcast.rb +223 -0
- data/lib/reactive_record/engine.rb +11 -0
- data/lib/reactive_record/interval.rb +190 -0
- data/lib/reactive_record/permissions.rb +117 -0
- data/lib/reactive_record/pry.rb +13 -0
- data/lib/reactive_record/reactive_scope.rb +18 -0
- data/lib/reactive_record/scope_description.rb +121 -0
- data/lib/reactive_record/serializers.rb +7 -0
- data/lib/reactive_record/server_data_cache.rb +478 -0
- data/path_release_steps.md +9 -0
- metadata +399 -109
- data/CODE_OF_CONDUCT.md +0 -49
- data/examples/chat-app/.gitignore +0 -21
- data/examples/chat-app/Gemfile +0 -62
- data/examples/chat-app/Gemfile.lock +0 -309
- data/examples/chat-app/README.md +0 -3
- data/examples/chat-app/Rakefile +0 -6
- data/examples/chat-app/app/assets/config/manifest.js +0 -3
- data/examples/chat-app/app/assets/javascripts/application.js +0 -3
- data/examples/chat-app/app/assets/stylesheets/application.scss +0 -33
- data/examples/chat-app/app/controllers/application_controller.rb +0 -3
- data/examples/chat-app/app/controllers/home_controller.rb +0 -5
- data/examples/chat-app/app/hyperloop/components/app.rb +0 -12
- data/examples/chat-app/app/hyperloop/components/formatted_div.rb +0 -15
- data/examples/chat-app/app/hyperloop/components/input_box.rb +0 -26
- data/examples/chat-app/app/hyperloop/components/message.rb +0 -29
- data/examples/chat-app/app/hyperloop/components/messages.rb +0 -8
- data/examples/chat-app/app/hyperloop/components/nav.rb +0 -30
- data/examples/chat-app/app/hyperloop/models/application_record.rb +0 -3
- data/examples/chat-app/app/hyperloop/models/message.rb +0 -6
- data/examples/chat-app/app/hyperloop/operations/operations.rb +0 -13
- data/examples/chat-app/app/hyperloop/stores/message_store.rb +0 -17
- data/examples/chat-app/app/policies/application_policy.rb +0 -9
- data/examples/chat-app/app/views/layouts/application.html.erb +0 -12
- data/examples/chat-app/bin/bundle +0 -3
- data/examples/chat-app/bin/rails +0 -9
- data/examples/chat-app/bin/rake +0 -9
- data/examples/chat-app/bin/setup +0 -34
- data/examples/chat-app/bin/spring +0 -17
- data/examples/chat-app/bin/update +0 -29
- data/examples/chat-app/config.ru +0 -5
- data/examples/chat-app/config/application.rb +0 -12
- data/examples/chat-app/config/boot.rb +0 -3
- data/examples/chat-app/config/cable.yml +0 -9
- data/examples/chat-app/config/database.yml +0 -25
- data/examples/chat-app/config/environment.rb +0 -5
- data/examples/chat-app/config/environments/development.rb +0 -56
- data/examples/chat-app/config/environments/production.rb +0 -86
- data/examples/chat-app/config/environments/test.rb +0 -42
- data/examples/chat-app/config/initializers/application_controller_renderer.rb +0 -6
- data/examples/chat-app/config/initializers/assets.rb +0 -11
- data/examples/chat-app/config/initializers/backtrace_silencers.rb +0 -7
- data/examples/chat-app/config/initializers/cookies_serializer.rb +0 -5
- data/examples/chat-app/config/initializers/filter_parameter_logging.rb +0 -4
- data/examples/chat-app/config/initializers/hyperloop.rb +0 -6
- data/examples/chat-app/config/initializers/inflections.rb +0 -16
- data/examples/chat-app/config/initializers/mime_types.rb +0 -4
- data/examples/chat-app/config/initializers/new_framework_defaults.rb +0 -24
- data/examples/chat-app/config/initializers/session_store.rb +0 -3
- data/examples/chat-app/config/initializers/wrap_parameters.rb +0 -14
- data/examples/chat-app/config/locales/en.yml +0 -23
- data/examples/chat-app/config/puma.rb +0 -47
- data/examples/chat-app/config/routes.rb +0 -5
- data/examples/chat-app/config/secrets.yml +0 -22
- data/examples/chat-app/config/spring.rb +0 -6
- data/examples/chat-app/db/migrate/20170319194429_create_message.rb +0 -9
- data/examples/chat-app/db/schema.rb +0 -48
- data/examples/chat-app/db/seeds.rb +0 -7
- data/examples/chat-app/lib/assets/.keep +0 -0
- data/examples/chat-app/lib/tasks/.keep +0 -0
- data/examples/chat-app/log/.keep +0 -0
- data/examples/chat-app/public/404.html +0 -67
- data/examples/chat-app/public/422.html +0 -67
- data/examples/chat-app/public/500.html +0 -66
- data/examples/chat-app/public/apple-touch-icon-precomposed.png +0 -0
- data/examples/chat-app/public/apple-touch-icon.png +0 -0
- data/examples/chat-app/public/favicon.ico +0 -0
- data/examples/chat-app/public/robots.txt +0 -5
- data/examples/chat-app/test/controllers/.keep +0 -0
- data/examples/chat-app/test/fixtures/.keep +0 -0
- data/examples/chat-app/test/fixtures/files/.keep +0 -0
- data/examples/chat-app/test/helpers/.keep +0 -0
- data/examples/chat-app/test/integration/.keep +0 -0
- data/examples/chat-app/test/mailers/.keep +0 -0
- data/examples/chat-app/test/models/.keep +0 -0
- data/examples/chat-app/test/test_helper.rb +0 -10
- data/examples/chat-app/tmp/.keep +0 -0
- data/examples/chat-app/vendor/assets/javascripts/.keep +0 -0
- data/examples/chat-app/vendor/assets/stylesheets/.keep +0 -0
- data/lib/hyperloop/model/version.rb +0 -5
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
module ReactiveRecord
|
|
2
|
+
|
|
3
|
+
class Collection
|
|
4
|
+
|
|
5
|
+
class DummySet
|
|
6
|
+
def new
|
|
7
|
+
@master ||= super
|
|
8
|
+
end
|
|
9
|
+
def method_missing(*args)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def unsaved_children
|
|
14
|
+
old_uc_already_being_called = @uc_already_being_called
|
|
15
|
+
if @owner && @association
|
|
16
|
+
@unsaved_children ||= Set.new
|
|
17
|
+
unless @uc_already_being_called
|
|
18
|
+
@uc_already_being_called = true
|
|
19
|
+
end
|
|
20
|
+
else
|
|
21
|
+
@unsaved_children ||= DummySet.new
|
|
22
|
+
end
|
|
23
|
+
@unsaved_children
|
|
24
|
+
ensure
|
|
25
|
+
@uc_already_being_called = old_uc_already_being_called
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def initialize(target_klass, owner = nil, association = nil, *vector)
|
|
29
|
+
@owner = owner # can be nil if this is an outer most scope
|
|
30
|
+
@association = association
|
|
31
|
+
@target_klass = target_klass
|
|
32
|
+
if owner and !owner.id and vector.length <= 1
|
|
33
|
+
@collection = []
|
|
34
|
+
elsif vector.length > 0
|
|
35
|
+
@vector = vector
|
|
36
|
+
elsif owner
|
|
37
|
+
@vector = owner.backing_record.vector + [association.attribute]
|
|
38
|
+
else
|
|
39
|
+
@vector = [target_klass]
|
|
40
|
+
end
|
|
41
|
+
@scopes = {}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def dup_for_sync
|
|
45
|
+
self.dup.instance_eval do
|
|
46
|
+
@collection = @collection.dup if @collection
|
|
47
|
+
@scopes = @scopes.dup
|
|
48
|
+
self
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def all
|
|
53
|
+
observed
|
|
54
|
+
@dummy_collection.notify if @dummy_collection
|
|
55
|
+
unless @collection
|
|
56
|
+
@collection = []
|
|
57
|
+
if ids = ReactiveRecord::Base.fetch_from_db([*@vector, "*all"])
|
|
58
|
+
ids.each do |id|
|
|
59
|
+
@collection << @target_klass.find_by(@target_klass.primary_key => id)
|
|
60
|
+
end
|
|
61
|
+
else
|
|
62
|
+
@dummy_collection = ReactiveRecord::Base.load_from_db(nil, *@vector, "*all")
|
|
63
|
+
@dummy_record = self[0]
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
@collection
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def [](index)
|
|
70
|
+
observed
|
|
71
|
+
if (@collection || all).length <= index and @dummy_collection
|
|
72
|
+
(@collection.length..index).each do |i|
|
|
73
|
+
new_dummy_record = ReactiveRecord::Base.new_from_vector(@target_klass, nil, *@vector, "*#{i}")
|
|
74
|
+
new_dummy_record.attributes[@association.inverse_of] = @owner if @association && !@association.through_association?
|
|
75
|
+
@collection << new_dummy_record
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
@collection[index]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def ==(other_collection)
|
|
82
|
+
observed
|
|
83
|
+
return !@collection unless other_collection.is_a? Collection
|
|
84
|
+
other_collection.observed
|
|
85
|
+
my_children = (@collection || []).select { |target| target != @dummy_record }
|
|
86
|
+
if other_collection
|
|
87
|
+
other_children = (other_collection.collection || []).select { |target| target != other_collection.dummy_record }
|
|
88
|
+
return false unless my_children == other_children
|
|
89
|
+
unsaved_children.to_a == other_collection.unsaved_children.to_a
|
|
90
|
+
else
|
|
91
|
+
my_children.empty? && unsaved_children.empty?
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
# todo move following to a separate module related to scope updates ******************
|
|
95
|
+
attr_reader :vector
|
|
96
|
+
attr_writer :scope_description
|
|
97
|
+
attr_writer :parent
|
|
98
|
+
attr_reader :pre_sync_related_records
|
|
99
|
+
|
|
100
|
+
def to_s
|
|
101
|
+
"<Coll-#{object_id} owner: #{@owner}, parent: #{@parent} - #{vector}>"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
class << self
|
|
105
|
+
|
|
106
|
+
=begin
|
|
107
|
+
sync_scopes takes a newly broadcasted record change and updates all relevant currently active scopes
|
|
108
|
+
This is particularly hard when the client proc is specified. For example consider this scope:
|
|
109
|
+
|
|
110
|
+
class TestModel < ApplicationRecord
|
|
111
|
+
scope :quicker, -> { where(completed: true) }, client: -> { completed }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
and this slice of reactive code:
|
|
115
|
+
|
|
116
|
+
DIV { "quicker.count = #{TestModel.quicker.count}" }
|
|
117
|
+
|
|
118
|
+
then on the server this code is executed:
|
|
119
|
+
|
|
120
|
+
TestModel.last.update(completed: false)
|
|
121
|
+
|
|
122
|
+
This will result in the changes being broadcast to the client, which may cauase the value of
|
|
123
|
+
TestModel.quicker.count to increase or decrease. Of course we may not actually have the all the records,
|
|
124
|
+
perhaps we just have the aggregate count.
|
|
125
|
+
|
|
126
|
+
To determine this sync_scopes first asks if the record being changed is in the scope given its value
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
=end
|
|
130
|
+
def sync_scopes(broadcast)
|
|
131
|
+
# record_with_current_values will return nil if data between
|
|
132
|
+
# the broadcast record and the value on the client is out of sync
|
|
133
|
+
# not running set_pre_sync_related_records will cause sync scopes
|
|
134
|
+
# to refresh all related scopes
|
|
135
|
+
React::State.bulk_update do
|
|
136
|
+
record = broadcast.record_with_current_values
|
|
137
|
+
apply_to_all_collections(
|
|
138
|
+
:set_pre_sync_related_records,
|
|
139
|
+
record, broadcast.new?
|
|
140
|
+
) if record
|
|
141
|
+
record = broadcast.record_with_new_values
|
|
142
|
+
apply_to_all_collections(
|
|
143
|
+
:sync_scopes,
|
|
144
|
+
record, record.destroyed?
|
|
145
|
+
)
|
|
146
|
+
record.backing_record.sync_unscoped_collection! if record.destroyed? || broadcast.new?
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def apply_to_all_collections(method, record, dont_gather)
|
|
151
|
+
related_records = Set.new if dont_gather
|
|
152
|
+
Base.outer_scopes.each do |collection|
|
|
153
|
+
unless dont_gather
|
|
154
|
+
related_records = collection.gather_related_records(record)
|
|
155
|
+
end
|
|
156
|
+
collection.send method, related_records, record
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def gather_related_records(record, related_records = Set.new)
|
|
162
|
+
merge_related_records(record, related_records)
|
|
163
|
+
live_scopes.each do |collection|
|
|
164
|
+
collection.gather_related_records(record, related_records)
|
|
165
|
+
end
|
|
166
|
+
related_records
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def merge_related_records(record, related_records)
|
|
170
|
+
if filter? && joins_with?(record)
|
|
171
|
+
related_records.merge(related_records_for(record))
|
|
172
|
+
end
|
|
173
|
+
related_records
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def filter?
|
|
177
|
+
true
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# is it necessary to check @association in the next 2 methods???
|
|
181
|
+
|
|
182
|
+
def joins_with?(record)
|
|
183
|
+
klass = record.class
|
|
184
|
+
if @association&.through_association
|
|
185
|
+
@association.through_association.klass == record.class
|
|
186
|
+
elsif @target_klass == klass
|
|
187
|
+
true
|
|
188
|
+
elsif !klass.inheritance_column
|
|
189
|
+
false
|
|
190
|
+
elsif klass.base_class == @target_class
|
|
191
|
+
klass < @target_klass
|
|
192
|
+
elsif klass.base_class == klass
|
|
193
|
+
@target_klass < klass
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def related_records_for(record)
|
|
198
|
+
return [] unless @association
|
|
199
|
+
attrs = record.attributes
|
|
200
|
+
return [] unless attrs[@association.inverse_of] == @owner
|
|
201
|
+
if !@association.through_association
|
|
202
|
+
[record]
|
|
203
|
+
elsif (source = attrs[@association.source])
|
|
204
|
+
[source]
|
|
205
|
+
else
|
|
206
|
+
[]
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def collector?
|
|
211
|
+
false
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def filter_records(related_records)
|
|
215
|
+
# possibly we should never get here???
|
|
216
|
+
scope_args = @vector.last.is_a?(Array) ? @vector.last[1..-1] : []
|
|
217
|
+
@scope_description.filter_records(related_records, scope_args)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def live_scopes
|
|
221
|
+
@live_scopes ||= Set.new
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def set_pre_sync_related_records(related_records, _record = nil)
|
|
225
|
+
#related_records = related_records.intersection([*@collection]) <- deleting this works
|
|
226
|
+
@pre_sync_related_records = related_records #in_this_collection related_records <- not sure if this works
|
|
227
|
+
live_scopes.each { |scope| scope.set_pre_sync_related_records(@pre_sync_related_records) }
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# NOTE sync_scopes is overridden in scope_description.rb
|
|
231
|
+
def sync_scopes(related_records, record, filtering = true)
|
|
232
|
+
#related_records = related_records.intersection([*@collection])
|
|
233
|
+
#related_records = in_this_collection related_records
|
|
234
|
+
live_scopes.each { |scope| scope.sync_scopes(related_records, record, filtering) }
|
|
235
|
+
notify_of_change unless related_records.empty?
|
|
236
|
+
ensure
|
|
237
|
+
@pre_sync_related_records = nil
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def apply_scope(name, *vector)
|
|
241
|
+
description = ScopeDescription.find(@target_klass, name)
|
|
242
|
+
collection = build_child_scope(description, *description.name, *vector)
|
|
243
|
+
collection.reload_from_db if name == "#{description.name}!"
|
|
244
|
+
collection
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def child_scopes
|
|
248
|
+
@child_scopes ||= {}
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def build_child_scope(scope_description, *scope_vector)
|
|
252
|
+
child_scopes[scope_vector] ||= begin
|
|
253
|
+
new_vector = @vector
|
|
254
|
+
new_vector += [scope_vector] unless new_vector.nil? || scope_vector.empty?
|
|
255
|
+
child_scope = Collection.new(@target_klass, nil, nil, *new_vector)
|
|
256
|
+
child_scope.scope_description = scope_description
|
|
257
|
+
child_scope.parent = self
|
|
258
|
+
child_scope.extend ScopedCollection
|
|
259
|
+
child_scope
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def link_to_parent
|
|
264
|
+
return if @linked
|
|
265
|
+
@linked = true
|
|
266
|
+
if @parent
|
|
267
|
+
@parent.link_child self
|
|
268
|
+
sync_collection_with_parent unless collection
|
|
269
|
+
else
|
|
270
|
+
ReactiveRecord::Base.add_to_outer_scopes self
|
|
271
|
+
end
|
|
272
|
+
all if collector? # force fetch all so the collector can do its job
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def link_child(child)
|
|
276
|
+
live_scopes << child
|
|
277
|
+
link_to_parent
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def sync_collection_with_parent
|
|
281
|
+
if @parent.collection
|
|
282
|
+
if @parent.collection.empty?
|
|
283
|
+
@collection = []
|
|
284
|
+
elsif filter?
|
|
285
|
+
@collection = filter_records(@parent.collection)
|
|
286
|
+
end
|
|
287
|
+
elsif @parent._count_internal(false).zero? # just changed this from count.zero?
|
|
288
|
+
@count = 0
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# end of stuff to move
|
|
293
|
+
|
|
294
|
+
def reload_from_db(force = nil)
|
|
295
|
+
if force || React::State.has_observers?(self, :collection)
|
|
296
|
+
@out_of_date = false
|
|
297
|
+
ReactiveRecord::Base.load_from_db(nil, *@vector, '*all') if @collection
|
|
298
|
+
ReactiveRecord::Base.load_from_db(nil, *@vector, '*count')
|
|
299
|
+
else
|
|
300
|
+
@out_of_date = true
|
|
301
|
+
end
|
|
302
|
+
self
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def observed
|
|
306
|
+
return if @observing || ReactiveRecord::Base.data_loading?
|
|
307
|
+
begin
|
|
308
|
+
@observing = true
|
|
309
|
+
link_to_parent
|
|
310
|
+
reload_from_db(true) if @out_of_date
|
|
311
|
+
React::State.get_state(self, :collection)
|
|
312
|
+
ensure
|
|
313
|
+
@observing = false
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def set_count_state(val)
|
|
318
|
+
unless ReactiveRecord::WhileLoading.has_observers?
|
|
319
|
+
React::State.set_state(self, :collection, collection, true)
|
|
320
|
+
end
|
|
321
|
+
@count = val
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _count_internal(load_from_client)
|
|
327
|
+
# when count is called on a leaf, count_internal is called for each
|
|
328
|
+
# ancestor. Only the outermost count has load_from_client == true
|
|
329
|
+
observed
|
|
330
|
+
if @collection
|
|
331
|
+
@collection.count
|
|
332
|
+
elsif @count ||= ReactiveRecord::Base.fetch_from_db([*@vector, "*count"])
|
|
333
|
+
@count
|
|
334
|
+
else
|
|
335
|
+
ReactiveRecord::Base.load_from_db(nil, *@vector, "*count") if load_from_client
|
|
336
|
+
@count = 1
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def count
|
|
341
|
+
_count_internal(true)
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
alias_method :length, :count
|
|
345
|
+
|
|
346
|
+
# WHY IS THIS NEEDED? Perhaps it was just for debug
|
|
347
|
+
def collect(*args, &block)
|
|
348
|
+
all.collect(*args, &block)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# def each_known_child
|
|
352
|
+
# [*collection, *client_pushes].each { |i| yield i }
|
|
353
|
+
# end
|
|
354
|
+
|
|
355
|
+
def proxy_association
|
|
356
|
+
@association || self # returning self allows this to work with things like Model.all
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def klass
|
|
360
|
+
@target_klass
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def push_and_update_belongs_to(id)
|
|
364
|
+
# example collection vector: TestModel.find(1).child_models.harrybarry
|
|
365
|
+
# harrybarry << child means that
|
|
366
|
+
# child.test_model = 1
|
|
367
|
+
# so... we go back starting at this collection and look for the first
|
|
368
|
+
# collection with an owner... that is our guy
|
|
369
|
+
child = proxy_association.klass.find(id)
|
|
370
|
+
push child
|
|
371
|
+
set_belongs_to child
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def set_belongs_to(child)
|
|
375
|
+
if @owner
|
|
376
|
+
# TODO this is major broken...current
|
|
377
|
+
child.send("#{@association.inverse_of}=", @owner) if @association && !@association.through_association
|
|
378
|
+
elsif @parent
|
|
379
|
+
@parent.set_belongs_to(child)
|
|
380
|
+
end
|
|
381
|
+
child
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
attr_reader :client_collection
|
|
385
|
+
|
|
386
|
+
# appointment.doctor = doctor_value (i.e. through association is changing)
|
|
387
|
+
# means appointment.doctor_value.patients << appointment.patient
|
|
388
|
+
# and we have to appointment.doctor(current value).patients.delete(appointment.patient)
|
|
389
|
+
|
|
390
|
+
def update_child(item)
|
|
391
|
+
backing_record = item.backing_record
|
|
392
|
+
if backing_record && @owner && @association && !@association.through_association? && item.attributes[@association.inverse_of] != @owner
|
|
393
|
+
inverse_of = @association.inverse_of
|
|
394
|
+
current_association = item.attributes[inverse_of]
|
|
395
|
+
backing_record.virgin = false unless backing_record.data_loading?
|
|
396
|
+
backing_record.update_belongs_to(inverse_of, @owner)
|
|
397
|
+
if current_association && current_association.attributes[@association.attribute]
|
|
398
|
+
current_association.attributes[@association.attribute].delete(item)
|
|
399
|
+
end
|
|
400
|
+
@owner.backing_record.sync_has_many(@association.attribute)
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def push(item)
|
|
405
|
+
item.itself # force get of at least the id
|
|
406
|
+
if collection
|
|
407
|
+
self.force_push item
|
|
408
|
+
else
|
|
409
|
+
unsaved_children << item
|
|
410
|
+
update_child(item)
|
|
411
|
+
@owner.backing_record.sync_has_many(@association.attribute) if @owner && @association
|
|
412
|
+
if !@count.nil?
|
|
413
|
+
@count += item.destroyed? ? -1 : 1
|
|
414
|
+
notify_of_change self
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
self
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
alias << push
|
|
421
|
+
|
|
422
|
+
def sort!(*args, &block)
|
|
423
|
+
replace(sort(*args, &block))
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def force_push(item)
|
|
427
|
+
return delete(item) if item.destroyed? # pushing a destroyed item is the same as removing it
|
|
428
|
+
all << item unless all.include? item # does this use == if so we are okay...
|
|
429
|
+
update_child(item)
|
|
430
|
+
if item.id and @dummy_record
|
|
431
|
+
@dummy_record.id = item.id
|
|
432
|
+
# we cant use == because that just means the objects are referencing
|
|
433
|
+
# the same backing record.
|
|
434
|
+
@collection.reject { |i| i.object_id == @dummy_record.object_id }
|
|
435
|
+
@dummy_record = @collection.detect { |r| r.backing_record.vector.last =~ /^\*[0-9]+$/ }
|
|
436
|
+
@dummy_collection = nil
|
|
437
|
+
end
|
|
438
|
+
notify_of_change self
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
[:first, :last].each do |method|
|
|
442
|
+
define_method method do |*args|
|
|
443
|
+
if args.count == 0
|
|
444
|
+
all.send(method)
|
|
445
|
+
else
|
|
446
|
+
apply_scope(method, *args)
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def replace(new_array)
|
|
452
|
+
unsaved_children.clear
|
|
453
|
+
new_array = new_array.to_a
|
|
454
|
+
return self if new_array == @collection
|
|
455
|
+
Base.load_data { internal_replace(new_array) }
|
|
456
|
+
notify_of_change new_array
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
def internal_replace(new_array)
|
|
460
|
+
|
|
461
|
+
# not tested if you do all[n] where n > 0... this will create additional dummy items, that this will not sync up.
|
|
462
|
+
# probably just moving things around so the @dummy_collection and @dummy_record are updated AFTER the new items are pushed
|
|
463
|
+
# should work.
|
|
464
|
+
|
|
465
|
+
if @dummy_collection
|
|
466
|
+
@dummy_collection.notify
|
|
467
|
+
array = new_array.is_a?(Collection) ? new_array.collection : new_array
|
|
468
|
+
@collection.each_with_index do |r, i|
|
|
469
|
+
r.id = new_array[i].id if array[i] and array[i].id and !r.new? and r.backing_record.vector.last =~ /^\*[0-9]+$/
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
@collection.dup.each { |item| delete(item) } if @collection # this line is a big nop I think
|
|
474
|
+
@collection = []
|
|
475
|
+
if new_array.is_a? Collection
|
|
476
|
+
@dummy_collection = new_array.dummy_collection
|
|
477
|
+
@dummy_record = new_array.dummy_record
|
|
478
|
+
new_array.collection.each { |item| self << item } if new_array.collection
|
|
479
|
+
else
|
|
480
|
+
@dummy_collection = @dummy_record = nil
|
|
481
|
+
new_array.each { |item| self << item }
|
|
482
|
+
end
|
|
483
|
+
notify_of_change new_array
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def delete(item)
|
|
487
|
+
unsaved_children.delete(item)
|
|
488
|
+
notify_of_change(
|
|
489
|
+
if @owner && @association && !@association.through_association?
|
|
490
|
+
inverse_of = @association.inverse_of
|
|
491
|
+
if (backing_record = item.backing_record) && item.attributes[inverse_of] == @owner
|
|
492
|
+
# the if prevents double update if delete is being called from << (see << above)
|
|
493
|
+
backing_record.update_belongs_to(inverse_of, nil)
|
|
494
|
+
end
|
|
495
|
+
delete_internal(item) { @owner.backing_record.sync_has_many(@association.attribute) }
|
|
496
|
+
else
|
|
497
|
+
delete_internal(item)
|
|
498
|
+
end
|
|
499
|
+
)
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def delete_internal(item)
|
|
503
|
+
if collection
|
|
504
|
+
all.delete(item)
|
|
505
|
+
elsif !@count.nil?
|
|
506
|
+
@count -= 1
|
|
507
|
+
end
|
|
508
|
+
yield if block_given? # was yield item, but item is not used
|
|
509
|
+
item
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
def loading?
|
|
513
|
+
all # need to force initialization at this point
|
|
514
|
+
@dummy_collection.loading?
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
def empty?
|
|
518
|
+
# should be handled by method missing below, but opal-rspec does not deal well
|
|
519
|
+
# with method missing, so to test...
|
|
520
|
+
all.empty?
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
def method_missing(method, *args, &block)
|
|
524
|
+
if [].respond_to? method
|
|
525
|
+
all.send(method, *args, &block)
|
|
526
|
+
elsif ScopeDescription.find(@target_klass, method)
|
|
527
|
+
apply_scope(method, *args)
|
|
528
|
+
elsif args.count == 1 && method.start_with?('find_by_')
|
|
529
|
+
apply_scope(:find_by, method.sub(/^find_by_/, '') => args.first)
|
|
530
|
+
elsif @target_klass.respond_to?(method) && ScopeDescription.find(@target_klass, "_#{method}")
|
|
531
|
+
apply_scope("_#{method}", *args).first
|
|
532
|
+
else
|
|
533
|
+
super
|
|
534
|
+
end
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
protected
|
|
538
|
+
|
|
539
|
+
def dummy_record
|
|
540
|
+
@dummy_record
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
def collection
|
|
544
|
+
@collection
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
def dummy_collection
|
|
548
|
+
@dummy_collection
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def notify_of_change(value = nil)
|
|
552
|
+
React::State.set_state(self, "collection", collection) unless ReactiveRecord::Base.data_loading?
|
|
553
|
+
value
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
end
|