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,75 @@
|
|
|
1
|
+
module ReactiveRecord
|
|
2
|
+
# ActiveRecord column access and conversion helpers
|
|
3
|
+
class Base
|
|
4
|
+
def columns_hash
|
|
5
|
+
model.columns_hash
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def self.column_type(column_hash)
|
|
9
|
+
column_hash && column_hash[:sql_type_metadata] && column_hash[:sql_type_metadata][:type]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def column_type(attr)
|
|
13
|
+
Base.column_type(columns_hash[attr])
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def convert_datetime(val)
|
|
17
|
+
if val.is_a?(Numeric)
|
|
18
|
+
Time.at(val)
|
|
19
|
+
elsif val.is_a?(Time)
|
|
20
|
+
val
|
|
21
|
+
else
|
|
22
|
+
Time.parse(val)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
alias convert_time convert_datetime
|
|
27
|
+
alias convert_timestamp convert_datetime
|
|
28
|
+
|
|
29
|
+
def convert_date(val)
|
|
30
|
+
if val.is_a?(Time)
|
|
31
|
+
Date.parse(val.strftime('%d/%m/%Y'))
|
|
32
|
+
elsif val.is_a?(Date)
|
|
33
|
+
val
|
|
34
|
+
else
|
|
35
|
+
Date.parse(val)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def convert_boolean(val)
|
|
40
|
+
!['false', false, nil, 0].include?(val)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def convert_integer(val)
|
|
44
|
+
Integer(`parseInt(#{val})`)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
alias convert_bigint convert_integer
|
|
48
|
+
|
|
49
|
+
def convert_float(val)
|
|
50
|
+
Float(val)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
alias convert_decimal convert_float
|
|
54
|
+
|
|
55
|
+
def convert_text(val)
|
|
56
|
+
val.to_s
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
alias convert_string convert_text
|
|
60
|
+
|
|
61
|
+
def self.serialized?
|
|
62
|
+
@serialized_attrs ||= Hash.new { |h, k| h[k] = Hash.new }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def convert(attr, val)
|
|
66
|
+
column_type = column_type(attr)
|
|
67
|
+
return val if self.class.serialized?[model][attr] ||
|
|
68
|
+
!column_type || val.loading? ||
|
|
69
|
+
(!val && column_type != :boolean)
|
|
70
|
+
conversion_method = "convert_#{column_type}"
|
|
71
|
+
return send(conversion_method, val) if respond_to? conversion_method
|
|
72
|
+
val
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# add mehods to Object to determine if this is a dummy object or not
|
|
2
|
+
class Object
|
|
3
|
+
def loaded?
|
|
4
|
+
!loading?
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def loading?
|
|
8
|
+
false
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def present?
|
|
12
|
+
!!self
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
module ReactiveRecord
|
|
17
|
+
class Base
|
|
18
|
+
# A DummyValue stands in for actual value while waiting for load from
|
|
19
|
+
# server. when value is accessed by most methods it notifies hyper-react
|
|
20
|
+
# so when the actual value loads react will update.
|
|
21
|
+
|
|
22
|
+
# DummyValue uses the ActiveRecord type info to act like an appropriate
|
|
23
|
+
# loaded value.
|
|
24
|
+
class DummyValue < BasicObject
|
|
25
|
+
def initialize(column_hash = nil)
|
|
26
|
+
column_hash ||= {}
|
|
27
|
+
notify
|
|
28
|
+
@column_hash = column_hash
|
|
29
|
+
column_type = Base.column_type(@column_hash) || 'nil'
|
|
30
|
+
default_value_method = "build_default_value_for_#{column_type}"
|
|
31
|
+
@object = __send__ default_value_method
|
|
32
|
+
rescue ::Exception
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def build_default_value_for_nil
|
|
36
|
+
@column_hash[:default] || nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def build_default_value_for_datetime
|
|
40
|
+
if @column_hash[:default]
|
|
41
|
+
::Time.parse(@column_hash[:default].gsub(' ','T')+'+00:00')
|
|
42
|
+
else
|
|
43
|
+
::ReactiveRecord::Base::DummyValue.dummy_time
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
alias build_default_value_for_time build_default_value_for_datetime
|
|
48
|
+
alias build_default_value_for_timestamp build_default_value_for_datetime
|
|
49
|
+
|
|
50
|
+
def build_default_value_for_date
|
|
51
|
+
if @column_hash[:default]
|
|
52
|
+
::Date.parse(@column_hash[:default])
|
|
53
|
+
else
|
|
54
|
+
::ReactiveRecord::Base::DummyValue.dummy_date
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def build_default_value_for_boolean
|
|
59
|
+
@column_hash[:default] || false
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def build_default_value_for_float
|
|
63
|
+
@column_hash[:default] || Float(0.0)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
alias build_default_value_for_decimal build_default_value_for_float
|
|
67
|
+
|
|
68
|
+
def build_default_value_for_integer
|
|
69
|
+
@column_hash[:default] || Integer(0)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
alias build_default_value_for_bigint build_default_value_for_integer
|
|
73
|
+
|
|
74
|
+
def build_default_value_for_string
|
|
75
|
+
@column_hash[:default] || ''
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
alias build_default_value_for_text build_default_value_for_string
|
|
79
|
+
|
|
80
|
+
def notify
|
|
81
|
+
return if ::ReactiveRecord::Base.data_loading?
|
|
82
|
+
::ReactiveRecord.loads_pending!
|
|
83
|
+
::ReactiveRecord::WhileLoading.loading!
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def loading?
|
|
87
|
+
true
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def loaded?
|
|
91
|
+
false
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def present?
|
|
95
|
+
false
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def nil?
|
|
99
|
+
true
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def !
|
|
103
|
+
true
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def method_missing(method, *args, &block)
|
|
107
|
+
if method.start_with?("build_default_value_for_")
|
|
108
|
+
nil
|
|
109
|
+
elsif @object || @object.respond_to?(method)
|
|
110
|
+
notify
|
|
111
|
+
@object.send method, *args, &block
|
|
112
|
+
elsif 0.respond_to? method
|
|
113
|
+
notify
|
|
114
|
+
0.send(method, *args, &block)
|
|
115
|
+
elsif ''.respond_to? method
|
|
116
|
+
notify
|
|
117
|
+
''.send(method, *args, &block)
|
|
118
|
+
else
|
|
119
|
+
super
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def coerce(s)
|
|
124
|
+
# notify # why are we not notifying here
|
|
125
|
+
return @object.coerce(s) if @object
|
|
126
|
+
[__send__("to_#{s.class.name.downcase}"), s]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def ==(other)
|
|
130
|
+
# notify # why are we not notifying here
|
|
131
|
+
other.object_id == object_id
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def object_id
|
|
135
|
+
`self.$$id`
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def is_a?(klass)
|
|
139
|
+
klass == ::ReactiveRecord::Base::DummyValue
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def zero?
|
|
143
|
+
return @object.zero? if @object
|
|
144
|
+
false
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def to_s
|
|
148
|
+
notify
|
|
149
|
+
return @object.to_s if @object
|
|
150
|
+
''
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def tap
|
|
154
|
+
yield self
|
|
155
|
+
self
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
alias inspect to_s
|
|
159
|
+
|
|
160
|
+
`#{self}.$$proto.toString = Opal.Object.$$proto.toString`
|
|
161
|
+
|
|
162
|
+
def to_f
|
|
163
|
+
notify
|
|
164
|
+
return @object.to_f if @object
|
|
165
|
+
0.0
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def to_i
|
|
169
|
+
notify
|
|
170
|
+
return @object.to_i if @object
|
|
171
|
+
0
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def to_numeric
|
|
175
|
+
notify
|
|
176
|
+
return @object.to_numeric if @object
|
|
177
|
+
0
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def to_number
|
|
181
|
+
notify
|
|
182
|
+
return @object.to_number if @object
|
|
183
|
+
0
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def self.dummy_time
|
|
187
|
+
@dummy_time ||= ::Time.parse('2001-01-01T00:00:00.000-00:00')
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def self.dummy_date
|
|
191
|
+
@dummy_date ||= ::Date.parse('1/1/2001')
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def to_date
|
|
195
|
+
notify
|
|
196
|
+
return @object.to_date if @object
|
|
197
|
+
::ReactiveRecord::Base::DummyValue.dummy_date
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def to_time
|
|
201
|
+
notify
|
|
202
|
+
return @object.to_time if @object
|
|
203
|
+
::ReactiveRecord::Base::DummyValue.dummy_time
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def acts_as_string?
|
|
207
|
+
return true if @object.is_a? ::String
|
|
208
|
+
return @object.acts_as_string? if @object && @object.respond_to?(:acts_as_string?)
|
|
209
|
+
true
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# this is a hackish way and compatible with any other rendered object
|
|
213
|
+
# to identify a DummyValue during render
|
|
214
|
+
# in ReactRenderingContext.run_child_block() and
|
|
215
|
+
# to convert it to a string, for rendering
|
|
216
|
+
# advantage over a try(:method) is, that it doesnt raise und thus is faster
|
|
217
|
+
# which is important during render
|
|
218
|
+
def respond_to?(method)
|
|
219
|
+
return true if method == :acts_as_string?
|
|
220
|
+
return true if %i[inspect to_date to_f to_i to_numeric to_number to_s to_time].include? method
|
|
221
|
+
return @object.respond_to? if @object
|
|
222
|
+
false
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def try(*args, &b)
|
|
226
|
+
if args.empty? && block_given?
|
|
227
|
+
yield self
|
|
228
|
+
else
|
|
229
|
+
__send__(*args, &b)
|
|
230
|
+
end
|
|
231
|
+
rescue ::Exception
|
|
232
|
+
nil
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
module ReactiveRecord
|
|
2
|
+
# creates getters for various method types
|
|
3
|
+
# TODO replace sync_attribute calls with direct logic
|
|
4
|
+
module Getters
|
|
5
|
+
def get_belongs_to(assoc, reload = nil)
|
|
6
|
+
getter_common(assoc.attribute, reload) do |has_key, attr|
|
|
7
|
+
return if new?
|
|
8
|
+
value = Base.fetch_from_db([@model, [:find, id], attr, @model.primary_key]) if id.present?
|
|
9
|
+
value = find_association(assoc, value)
|
|
10
|
+
sync_ignore_dummy attr, value, has_key
|
|
11
|
+
end&.cast_to_current_sti_type
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def get_has_many(assoc, reload = nil)
|
|
15
|
+
getter_common(assoc.attribute, reload) do |_has_key, attr|
|
|
16
|
+
if new?
|
|
17
|
+
@attributes[attr] = Collection.new(assoc.klass, @ar_instance, assoc)
|
|
18
|
+
else
|
|
19
|
+
sync_attribute attr, Collection.new(assoc.klass, @ar_instance, assoc, *vector, attr)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def get_attr_value(attr, reload = nil)
|
|
25
|
+
non_relationship_getter_common(attr, reload) do
|
|
26
|
+
sync_attribute attr, convert(attr, model.columns_hash[attr][:default])
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def get_primary_key_value
|
|
31
|
+
non_relationship_getter_common(model.primary_key, false)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def get_server_method(attr, reload = nil)
|
|
35
|
+
non_relationship_getter_common(attr, reload) do |has_key|
|
|
36
|
+
sync_ignore_dummy attr, Base.load_from_db(self, *(vector ? vector : [nil]), attr), has_key
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def get_ar_aggregate(aggr, reload = nil)
|
|
41
|
+
getter_common(aggr.attribute, reload) do |has_key, attr|
|
|
42
|
+
if new?
|
|
43
|
+
@attributes[attr] = aggr.klass.new.backing_record.link_aggregate(attr, self)
|
|
44
|
+
else
|
|
45
|
+
sync_ignore_dummy attr, new_from_vector(aggr.klass, self, *vector, attr), has_key
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def get_non_ar_aggregate(attr, reload = nil)
|
|
51
|
+
non_relationship_getter_common(attr, reload)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def virtual_fetch_on_server_warning(attr)
|
|
57
|
+
log(
|
|
58
|
+
"Warning fetching virtual attributes (#{model.name}.#{attr}) during prerendering "\
|
|
59
|
+
'on a changed or new model is not implemented.',
|
|
60
|
+
:warning
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def sync_ignore_dummy(attr, value, has_key)
|
|
65
|
+
# ignore the value if its a Dummy value and there is already a value present
|
|
66
|
+
# this is used to implement reloading. During the reload while we are waiting we
|
|
67
|
+
# want the current attribute (if its present) to not change. Once the fetch
|
|
68
|
+
# is complete the fetch process will reload the attribute
|
|
69
|
+
value = @attributes[attr] if has_key && value.is_a?(Base::DummyValue)
|
|
70
|
+
sync_attribute(attr, value)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def non_relationship_getter_common(attr, reload, &block)
|
|
74
|
+
getter_common(attr, reload) do |has_key|
|
|
75
|
+
if new?
|
|
76
|
+
yield has_key if block
|
|
77
|
+
elsif on_opal_client?
|
|
78
|
+
sync_ignore_dummy attr, Base.load_from_db(self, *(vector ? vector : [nil]), attr), has_key
|
|
79
|
+
elsif id.present?
|
|
80
|
+
sync_attribute attr, Base.fetch_from_db([@model, [:find, id], attr])
|
|
81
|
+
else
|
|
82
|
+
sync_attribute attr, Base.fetch_from_db([*vector, attr])
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def getter_common(attribute, reload)
|
|
88
|
+
@virgin = false unless data_loading?
|
|
89
|
+
return if @destroyed
|
|
90
|
+
if @attributes.key? attribute
|
|
91
|
+
current_value = @attributes[attribute]
|
|
92
|
+
current_value.notify if current_value.is_a? Base::DummyValue
|
|
93
|
+
if reload
|
|
94
|
+
virtual_fetch_on_server_warning(attribute) if on_opal_server? && changed?
|
|
95
|
+
yield true, attribute
|
|
96
|
+
else
|
|
97
|
+
current_value
|
|
98
|
+
end
|
|
99
|
+
else
|
|
100
|
+
virtual_fetch_on_server_warning(attribute) if on_opal_server? && changed?
|
|
101
|
+
yield false, attribute
|
|
102
|
+
end.tap { |value| React::State.get_state(self, attribute) unless data_loading? }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def find_association(association, id)
|
|
106
|
+
inverse_of = association.inverse_of
|
|
107
|
+
instance = if id
|
|
108
|
+
find(association.klass, association.klass.primary_key => id)
|
|
109
|
+
else
|
|
110
|
+
new_from_vector(association.klass, nil, *vector, association.attribute)
|
|
111
|
+
end
|
|
112
|
+
instance_backing_record_attributes = instance.attributes
|
|
113
|
+
inverse_association = association.klass.reflect_on_association(inverse_of)
|
|
114
|
+
if inverse_association.collection?
|
|
115
|
+
instance_backing_record_attributes[inverse_of] = if id and id != ""
|
|
116
|
+
Collection.new(@model, instance, inverse_association, association.klass, ["find", id], inverse_of)
|
|
117
|
+
else
|
|
118
|
+
Collection.new(@model, instance, inverse_association, *vector, association.attribute, inverse_of)
|
|
119
|
+
end unless instance_backing_record_attributes[inverse_of]
|
|
120
|
+
instance_backing_record_attributes[inverse_of].replace [@ar_instance]
|
|
121
|
+
else
|
|
122
|
+
instance_backing_record_attributes[inverse_of] = @ar_instance
|
|
123
|
+
end unless association.through_association? || instance_backing_record_attributes.key?(inverse_of)
|
|
124
|
+
instance
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def link_aggregate(attr, parent)
|
|
128
|
+
self.aggregate_owner = parent
|
|
129
|
+
self.aggregate_attribute = attr
|
|
130
|
+
@ar_instance
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
module ReactiveRecord
|
|
4
|
+
|
|
5
|
+
class Base
|
|
6
|
+
|
|
7
|
+
include React::IsomorphicHelpers
|
|
8
|
+
|
|
9
|
+
before_first_mount do |context|
|
|
10
|
+
if RUBY_ENGINE != 'opal'
|
|
11
|
+
@server_data_cache = ReactiveRecord::ServerDataCache.new(context.controller.acting_user, {})
|
|
12
|
+
else
|
|
13
|
+
@public_columns_hash = get_public_columns_hash
|
|
14
|
+
define_attribute_methods
|
|
15
|
+
@outer_scopes = Set.new
|
|
16
|
+
@fetch_scheduled = nil
|
|
17
|
+
initialize_lookup_tables
|
|
18
|
+
if on_opal_client?
|
|
19
|
+
@pending_fetches = []
|
|
20
|
+
@pending_records = []
|
|
21
|
+
#@current_fetch_id = nil
|
|
22
|
+
unless `typeof window.ReactiveRecordInitialData === 'undefined'`
|
|
23
|
+
log(["Reactive record prerendered data being loaded: %o", `window.ReactiveRecordInitialData`])
|
|
24
|
+
JSON.from_object(`window.ReactiveRecordInitialData`).each do |hash|
|
|
25
|
+
load_from_json hash
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.deprecation_warning(model, message)
|
|
33
|
+
@deprecation_messages ||= []
|
|
34
|
+
message = "Warning: Deprecated feature used in #{model}. #{message}"
|
|
35
|
+
unless @deprecation_messages.include? message
|
|
36
|
+
@deprecation_messages << message
|
|
37
|
+
log message, :warning
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def deprecation_warning(message)
|
|
42
|
+
self.class.deprecation_warning(model, message)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def records
|
|
46
|
+
self.class.instance_variable_get(:@records)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Prerendering db access (returns nil if on client):
|
|
50
|
+
# at end of prerendering dumps all accessed records in the footer
|
|
51
|
+
|
|
52
|
+
isomorphic_method(:fetch_from_db) do |f, vector|
|
|
53
|
+
# vector must end with either "*all", or be a simple attribute
|
|
54
|
+
f.send_to_server [vector.shift.name, *vector] if RUBY_ENGINE == 'opal'
|
|
55
|
+
f.when_on_server { @server_data_cache[*vector] }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
isomorphic_method(:find_in_db) do |f, klass, attrs|
|
|
59
|
+
f.send_to_server klass.name, attrs if RUBY_ENGINE == 'opal'
|
|
60
|
+
f.when_on_server { @server_data_cache[klass, ['find_by', attrs], 'id'] }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
class << self
|
|
64
|
+
attr_reader :public_columns_hash
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def self.define_attribute_methods
|
|
68
|
+
public_columns_hash.keys.each do |model|
|
|
69
|
+
Object.const_get(model).define_attribute_methods rescue nil
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
isomorphic_method(:get_public_columns_hash) do |f|
|
|
74
|
+
f.when_on_client { JSON.parse(`JSON.stringify(window.ReactiveRecordPublicColumnsHash)`) }
|
|
75
|
+
f.send_to_server
|
|
76
|
+
f.when_on_server { ActiveRecord::Base.public_columns_hash }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
prerender_footer do
|
|
80
|
+
if @server_data_cache
|
|
81
|
+
json = @server_data_cache.as_json.to_json # can this just be to_json?
|
|
82
|
+
@server_data_cache.clear_requests
|
|
83
|
+
else
|
|
84
|
+
json = {}.to_json
|
|
85
|
+
end
|
|
86
|
+
"<script type='text/javascript'>\n"+
|
|
87
|
+
"if (typeof window.ReactiveRecordPublicColumnsHash === 'undefined') { \n" +
|
|
88
|
+
" window.ReactiveRecordPublicColumnsHash = #{ActiveRecord::Base.public_columns_hash_as_json}}\n" +
|
|
89
|
+
"if (typeof window.ReactiveRecordInitialData === 'undefined') { window.ReactiveRecordInitialData = [] }\n" +
|
|
90
|
+
"window.ReactiveRecordInitialData.push(#{json})\n"+
|
|
91
|
+
"</script>\n"
|
|
92
|
+
end if RUBY_ENGINE != 'opal'
|
|
93
|
+
|
|
94
|
+
# Client side db access (never called during prerendering):
|
|
95
|
+
|
|
96
|
+
# Always returns an object of class DummyValue which will act like most standard AR field types
|
|
97
|
+
# Whenever a dummy value is accessed it notify React that there are loads pending so appropriate rerenders
|
|
98
|
+
# will occur when the value is eventually loaded.
|
|
99
|
+
|
|
100
|
+
# queue up fetches, and at the end of each rendering cycle fetch the records
|
|
101
|
+
# notify that loads are pending
|
|
102
|
+
|
|
103
|
+
def self.load_from_db(record, *vector)
|
|
104
|
+
return nil unless on_opal_client? # this can happen when we are on the server and a nil value is returned for an attribute
|
|
105
|
+
# only called from the client side
|
|
106
|
+
# pushes the value of vector onto the a list of vectors that will be loaded from the server when the next
|
|
107
|
+
# rendering cycle completes.
|
|
108
|
+
# takes care of informing react that there are things to load, and schedules the loader to run
|
|
109
|
+
# Note there is no equivilent to find_in_db, because each vector implicitly does a find.
|
|
110
|
+
raise "attempt to do a find_by_id of nil. This will return all records, and is not allowed" if vector[1] == ["find_by_id", nil]
|
|
111
|
+
vector = [record.model.model_name.to_s, ["new", record.object_id]]+vector[1..-1] if vector[0].nil?
|
|
112
|
+
unless data_loading?
|
|
113
|
+
@pending_fetches << vector
|
|
114
|
+
@pending_records << record if record
|
|
115
|
+
schedule_fetch
|
|
116
|
+
end
|
|
117
|
+
DummyValue.new(record && record.get_columns_info_for_vector(vector))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def get_columns_info_for_vector(vector)
|
|
121
|
+
method_name = vector.last
|
|
122
|
+
method_name = method_name.first if method_name.is_a? Array
|
|
123
|
+
model.columns_hash[method_name] || model.server_methods[method_name]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class << self
|
|
128
|
+
|
|
129
|
+
attr_reader :pending_fetches
|
|
130
|
+
attr_reader :current_fetch_id
|
|
131
|
+
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def self.schedule_fetch
|
|
135
|
+
React::State.set_state(WhileLoading, :quiet, false) # moved from while loading module see loading! method
|
|
136
|
+
return if @fetch_scheduled
|
|
137
|
+
@current_fetch_id = Time.now
|
|
138
|
+
@fetch_scheduled = after(0) do
|
|
139
|
+
# Skip the fetch if there are no pending_fetches. This would never normally happen
|
|
140
|
+
# but during testing we might reset the context while there are pending fetches
|
|
141
|
+
next unless @pending_fetches.count > 0
|
|
142
|
+
saved_current_fetch_id = @current_fetch_id
|
|
143
|
+
saved_pending_fetches = @pending_fetches.uniq
|
|
144
|
+
models, associations = gather_records(@pending_records, false, nil)
|
|
145
|
+
log(["Server Fetching: %o", saved_pending_fetches.to_n])
|
|
146
|
+
start_time = `Date.now()`
|
|
147
|
+
Operations::Fetch.run(models: models, associations: associations, pending_fetches: saved_pending_fetches)
|
|
148
|
+
.then do |response|
|
|
149
|
+
begin
|
|
150
|
+
fetch_time = `Date.now()`
|
|
151
|
+
log(" Fetched in: #{`(fetch_time - start_time)/ 1000`}s")
|
|
152
|
+
timer = after(0) do
|
|
153
|
+
log(" Processed in: #{`(Date.now() - fetch_time) / 1000`}s")
|
|
154
|
+
log([' Returned: %o', response.to_n])
|
|
155
|
+
end
|
|
156
|
+
begin
|
|
157
|
+
ReactiveRecord::Base.load_from_json(response)
|
|
158
|
+
rescue Exception => e
|
|
159
|
+
`clearTimeout(#{timer})`
|
|
160
|
+
log("Unexpected exception raised while loading json from server: #{e}", :error)
|
|
161
|
+
end
|
|
162
|
+
ReactiveRecord.run_blocks_to_load saved_current_fetch_id
|
|
163
|
+
ensure
|
|
164
|
+
ReactiveRecord::WhileLoading.loaded_at saved_current_fetch_id
|
|
165
|
+
ReactiveRecord::WhileLoading.quiet! if @pending_fetches.empty?
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
.fail do |response|
|
|
169
|
+
log("Fetch failed", :error)
|
|
170
|
+
begin
|
|
171
|
+
ReactiveRecord.run_blocks_to_load(saved_current_fetch_id, response)
|
|
172
|
+
ensure
|
|
173
|
+
ReactiveRecord::WhileLoading.quiet! if @pending_fetches.empty?
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
@pending_fetches = []
|
|
177
|
+
@pending_records = []
|
|
178
|
+
@fetch_scheduled = nil
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def self.get_type_hash(record)
|
|
183
|
+
{record.class.inheritance_column => record[record.class.inheritance_column]}
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
if RUBY_ENGINE == 'opal'
|
|
187
|
+
|
|
188
|
+
def self.gather_records(records_to_process, force, record_being_saved)
|
|
189
|
+
# we want to pass not just the model data to save, but also enough information so that on return from the server
|
|
190
|
+
# we can update the models on the client
|
|
191
|
+
|
|
192
|
+
# input
|
|
193
|
+
# list of records to process, will grow as we chase associations
|
|
194
|
+
# outputs
|
|
195
|
+
models = [] # the actual data to save {id: record.object_id, model: record.model.model_name.to_s, attributes: changed_attributes}
|
|
196
|
+
associations = [] # {parent_id: record.object_id, attribute: attribute, child_id: assoc_record.object_id}
|
|
197
|
+
|
|
198
|
+
# used to keep track of records that have been processed for effeciency
|
|
199
|
+
# for quick lookup of records that have been or will be processed [record.object_id] => record
|
|
200
|
+
records_to_process = records_to_process.uniq
|
|
201
|
+
backing_records = Hash[*records_to_process.collect { |record| [record.object_id, record] }.flatten(1)]
|
|
202
|
+
|
|
203
|
+
add_new_association = lambda do |record, attribute, assoc_record|
|
|
204
|
+
unless backing_records[assoc_record.object_id]
|
|
205
|
+
records_to_process << assoc_record
|
|
206
|
+
backing_records[assoc_record.object_id] = assoc_record
|
|
207
|
+
end
|
|
208
|
+
associations << {parent_id: record.object_id, attribute: attribute, child_id: assoc_record.object_id}
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
record_index = 0
|
|
212
|
+
while(record_index < records_to_process.count)
|
|
213
|
+
record = records_to_process[record_index]
|
|
214
|
+
if record.id.loading? and record_being_saved
|
|
215
|
+
raise "Attempt to save a model while it or an associated model is still loading: model being saved: #{record_being_saved.model}:#{record_being_saved.id}#{', associated model: '+record.model.to_s if record != record_being_saved}"
|
|
216
|
+
end
|
|
217
|
+
output_attributes = {record.model.primary_key => record.id.loading? ? nil : record.id}
|
|
218
|
+
vector = record.vector || [record.model.model_name.to_s, ["new", record.object_id]]
|
|
219
|
+
models << {id: record.object_id, model: record.model.model_name.to_s, attributes: output_attributes, vector: vector}
|
|
220
|
+
record.attributes.each do |attribute, value|
|
|
221
|
+
if association = record.model.reflect_on_association(attribute)
|
|
222
|
+
if association.collection?
|
|
223
|
+
# following line changed from .all to .collection on 10/28
|
|
224
|
+
[*value.collection, *value.unsaved_children].each do |assoc|
|
|
225
|
+
add_new_association.call(record, attribute, assoc.backing_record) if assoc.changed?(association.inverse_of) or assoc.new?
|
|
226
|
+
end
|
|
227
|
+
elsif record.new? || record.changed?(attribute) || (record == record_being_saved && force)
|
|
228
|
+
if value.nil?
|
|
229
|
+
output_attributes[attribute] = nil
|
|
230
|
+
else
|
|
231
|
+
add_new_association.call record, attribute, value.backing_record
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
elsif aggregation = record.model.reflect_on_aggregation(attribute) and (aggregation.klass < ActiveRecord::Base)
|
|
235
|
+
add_new_association.call record, attribute, value.backing_record unless value.nil?
|
|
236
|
+
elsif aggregation
|
|
237
|
+
new_value = aggregation.serialize(value)
|
|
238
|
+
output_attributes[attribute] = new_value if record.changed?(attribute) or new_value != aggregation.serialize(record.synced_attributes[attribute])
|
|
239
|
+
elsif record.new? or record.changed?(attribute)
|
|
240
|
+
output_attributes[attribute] = value
|
|
241
|
+
end
|
|
242
|
+
end if record.new? || record.changed? || (record == record_being_saved && force)
|
|
243
|
+
record_index += 1
|
|
244
|
+
end
|
|
245
|
+
[models, associations, backing_records]
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def save_or_validate(save, validate, force, &block)
|
|
249
|
+
if data_loading?
|
|
250
|
+
sync!
|
|
251
|
+
elsif force || changed? || (validate && new?)
|
|
252
|
+
HyperMesh.load do
|
|
253
|
+
ReactiveRecord.loads_pending! unless self.class.pending_fetches.empty?
|
|
254
|
+
end.then { send_save_to_server(save, validate, force, &block) }
|
|
255
|
+
#save_to_server(validate, force, &block)
|
|
256
|
+
else
|
|
257
|
+
promise = Promise.new
|
|
258
|
+
yield true, nil, [] if block
|
|
259
|
+
promise.resolve({success: true})
|
|
260
|
+
promise
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def send_save_to_server(save, validate, force, &block)
|
|
265
|
+
models, associations, backing_records = self.class.gather_records([self], force, self)
|
|
266
|
+
|
|
267
|
+
begin
|
|
268
|
+
backing_records.each { |id, record| record.saving! } if save
|
|
269
|
+
|
|
270
|
+
promise = Promise.new
|
|
271
|
+
Operations::Save.run(models: models, associations: associations, save: save, validate: validate)
|
|
272
|
+
.then do |response|
|
|
273
|
+
begin
|
|
274
|
+
response[:models] = response[:saved_models].collect do |item|
|
|
275
|
+
backing_records[item[0]].ar_instance
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
if save
|
|
279
|
+
if response[:success]
|
|
280
|
+
response[:saved_models].each do |item|
|
|
281
|
+
Broadcast.to_self backing_records[item[0]].ar_instance, item[2]
|
|
282
|
+
end
|
|
283
|
+
else
|
|
284
|
+
log(response[:message], :error)
|
|
285
|
+
response[:saved_models].each do |item|
|
|
286
|
+
log(" Model: #{item[1]}[#{item[0]}] Attributes: #{item[2]} Errors: #{item[3]}", :error) if item[3]
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
response[:saved_models].each do | item |
|
|
292
|
+
backing_records[item[0]].sync_unscoped_collection! if save
|
|
293
|
+
backing_records[item[0]].errors! item[3]
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
yield response[:success], response[:message], response[:models] if block
|
|
297
|
+
promise.resolve response # TODO this could be problematic... there was no .json here, so .... what's to do?
|
|
298
|
+
|
|
299
|
+
rescue Exception => e
|
|
300
|
+
# debugger
|
|
301
|
+
log("Exception raised while saving - #{e}", :error)
|
|
302
|
+
ensure
|
|
303
|
+
backing_records.each { |_id, record| record.saved! rescue nil } if save
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
promise
|
|
307
|
+
rescue Exception => e
|
|
308
|
+
backing_records.each { |_id, record| record.saved!(true) rescue nil } if save
|
|
309
|
+
end
|
|
310
|
+
rescue Exception => e
|
|
311
|
+
debugger
|
|
312
|
+
log("Exception raised while saving - #{e}", :error)
|
|
313
|
+
yield false, e.message, [] if block
|
|
314
|
+
promise.resolve({success: false, message: e.message, models: []})
|
|
315
|
+
promise
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
else
|
|
319
|
+
|
|
320
|
+
def self.find_record(model, id, vector, save)
|
|
321
|
+
if !save
|
|
322
|
+
found = vector[1..-1].inject(vector[0]) do |object, method|
|
|
323
|
+
if object.nil? # happens if you try to do an all on empty scope followed by more scopes
|
|
324
|
+
object
|
|
325
|
+
elsif method.is_a? Array
|
|
326
|
+
if method[0] == 'new'
|
|
327
|
+
object.new
|
|
328
|
+
else
|
|
329
|
+
object.send(*method)
|
|
330
|
+
end
|
|
331
|
+
elsif method.is_a? String and method[0] == '*'
|
|
332
|
+
object[method.gsub(/^\*/,'').to_i]
|
|
333
|
+
else
|
|
334
|
+
object.send(method)
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
if id and (found.nil? or !(found.class <= model) or (found.id and found.id.to_s != id.to_s))
|
|
338
|
+
raise "Inconsistent data sent to server - #{model.name}.find(#{id}) != [#{vector}]"
|
|
339
|
+
end
|
|
340
|
+
found
|
|
341
|
+
elsif id
|
|
342
|
+
model.find(id)
|
|
343
|
+
else
|
|
344
|
+
model.new
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def self.is_enum?(record, key)
|
|
350
|
+
record.class.respond_to?(:defined_enums) && record.class.defined_enums[key]
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def self.save_records(models, associations, acting_user, validate, save)
|
|
354
|
+
reactive_records = {}
|
|
355
|
+
vectors = {}
|
|
356
|
+
new_models = []
|
|
357
|
+
saved_models = []
|
|
358
|
+
dont_save_list = []
|
|
359
|
+
|
|
360
|
+
models.each do |model_to_save|
|
|
361
|
+
attributes = model_to_save[:attributes]
|
|
362
|
+
model = Object.const_get(model_to_save[:model])
|
|
363
|
+
id = attributes.delete(model.primary_key) if model.respond_to? :primary_key # if we are saving existing model primary key value will be present
|
|
364
|
+
vector = model_to_save[:vector]
|
|
365
|
+
vector = [vector[0].constantize] + vector[1..-1].collect do |method|
|
|
366
|
+
if method.is_a?(Array) and method.first == "find_by_id"
|
|
367
|
+
["find", method.last]
|
|
368
|
+
else
|
|
369
|
+
method
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
reactive_records[model_to_save[:id]] = vectors[vector] = record = find_record(model, id, vector, save) # ??? || validate ???
|
|
373
|
+
next unless record
|
|
374
|
+
if attributes.empty?
|
|
375
|
+
dont_save_list << record unless save
|
|
376
|
+
elsif record.respond_to?(:id) && record.id
|
|
377
|
+
# we have an already exising activerecord model
|
|
378
|
+
keys = record.attributes.keys
|
|
379
|
+
attributes.each do |key, value|
|
|
380
|
+
if is_enum?(record, key)
|
|
381
|
+
record.send("#{key}=", value)
|
|
382
|
+
elsif keys.include? key
|
|
383
|
+
record[key] = value
|
|
384
|
+
elsif value && (aggregation = record.class.reflect_on_aggregation(key.to_sym)) && !(aggregation.klass < ActiveRecord::Base)
|
|
385
|
+
aggregation.mapping.each_with_index do |pair, i|
|
|
386
|
+
record[pair.first] = value[i]
|
|
387
|
+
end
|
|
388
|
+
elsif record.respond_to? "#{key}="
|
|
389
|
+
record.send("#{key}=", value)
|
|
390
|
+
else
|
|
391
|
+
# TODO once reading schema.rb on client is implemented throw an error here
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
else
|
|
395
|
+
# either the model is new, or its not even an active record model
|
|
396
|
+
dont_save_list << record unless save
|
|
397
|
+
keys = record.attributes.keys
|
|
398
|
+
attributes.each do |key, value|
|
|
399
|
+
if is_enum?(record, key)
|
|
400
|
+
record.send("#{key}=",value)
|
|
401
|
+
elsif keys.include? key
|
|
402
|
+
record[key] = value
|
|
403
|
+
elsif !value.nil? and aggregation = record.class.reflect_on_aggregation(key) and !(aggregation.klass < ActiveRecord::Base)
|
|
404
|
+
aggregation.mapping.each_with_index do |pair, i|
|
|
405
|
+
record[pair.first] = value[i]
|
|
406
|
+
end
|
|
407
|
+
elsif key.to_s != "id" and record.respond_to?("#{key}=") # server side methods can get included and we won't be able to write them...
|
|
408
|
+
# for example if you have a server side method foo, that you "get" on a new record, then later that value will get sent to the server
|
|
409
|
+
# we should track better these server side methods so this does not happen
|
|
410
|
+
record.send("#{key}=",value)
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
new_models << record
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
#puts "!!!!!!!!!!!!!!attributes updated"
|
|
418
|
+
ActiveRecord::Base.transaction do
|
|
419
|
+
associations.each do |association|
|
|
420
|
+
parent = reactive_records[association[:parent_id]]
|
|
421
|
+
next unless parent
|
|
422
|
+
#parent.instance_variable_set("@reactive_record_#{association[:attribute]}_changed", true) remove this????
|
|
423
|
+
if parent.class.reflect_on_aggregation(association[:attribute].to_sym)
|
|
424
|
+
#puts ">>>>>>AGGREGATE>>>> #{parent.class.name}.send('#{association[:attribute]}=', #{reactive_records[association[:child_id]]})"
|
|
425
|
+
aggregate = reactive_records[association[:child_id]]
|
|
426
|
+
dont_save_list << aggregate
|
|
427
|
+
current_attributes = parent.send(association[:attribute]).attributes
|
|
428
|
+
#puts "current parent attributes = #{current_attributes}"
|
|
429
|
+
new_attributes = aggregate.attributes
|
|
430
|
+
#puts "current child attributes = #{new_attributes}"
|
|
431
|
+
merged_attributes = current_attributes.merge(new_attributes) { |k, current_attr, new_attr| aggregate.send("#{k}_changed?") ? new_attr : current_attr}
|
|
432
|
+
#puts "merged attributes = #{merged_attributes}"
|
|
433
|
+
aggregate.assign_attributes(merged_attributes)
|
|
434
|
+
#puts "aggregate attributes after merge = #{aggregate.attributes}"
|
|
435
|
+
parent.send("#{association[:attribute]}=", aggregate)
|
|
436
|
+
#puts "updated is frozen? #{aggregate.frozen?}, parent attributes = #{parent.send(association[:attribute]).attributes}"
|
|
437
|
+
elsif parent.class.reflect_on_association(association[:attribute].to_sym).nil?
|
|
438
|
+
raise "Missing association :#{association[:attribute]} for #{parent.class.name}. Was association defined on opal side only?"
|
|
439
|
+
elsif parent.class.reflect_on_association(association[:attribute].to_sym).collection?
|
|
440
|
+
#puts ">>>>>>>>>> #{parent.class.name}.send('#{association[:attribute]}') << #{reactive_records[association[:child_id]]})"
|
|
441
|
+
dont_save_list.delete(parent)
|
|
442
|
+
#if false and parent.new?
|
|
443
|
+
#parent.send("#{association[:attribute]}") << reactive_records[association[:child_id]]
|
|
444
|
+
# puts "updated"
|
|
445
|
+
#else
|
|
446
|
+
#puts "skipped"
|
|
447
|
+
#end
|
|
448
|
+
else
|
|
449
|
+
#puts ">>>>ASSOCIATION>>>> #{parent.class.name}.send('#{association[:attribute]}=', #{reactive_records[association[:child_id]]})"
|
|
450
|
+
parent.send("#{association[:attribute]}=", reactive_records[association[:child_id]])
|
|
451
|
+
dont_save_list.delete(parent)
|
|
452
|
+
#puts "updated"
|
|
453
|
+
end
|
|
454
|
+
end if associations
|
|
455
|
+
|
|
456
|
+
# get rid of any records that don't require further processing, as a side effect
|
|
457
|
+
# we also save any records that need to be saved (these may be rolled back later.)
|
|
458
|
+
|
|
459
|
+
reactive_records.keep_if do |reactive_record_id, record|
|
|
460
|
+
next false unless record # throw out items where we couldn't find a record
|
|
461
|
+
next true if record.frozen? # skip (but process later) frozen records
|
|
462
|
+
next true if dont_save_list.include?(record) # skip if the record is on the don't save list
|
|
463
|
+
next true if record.changed.include?(record.class.primary_key) # happens on an aggregate
|
|
464
|
+
next false if record.id && !record.changed? # throw out any existing records with no changes
|
|
465
|
+
# if we get to here save the record and return true to keep it
|
|
466
|
+
op = new_models.include?(record) ? :create_permitted? : :update_permitted?
|
|
467
|
+
record.check_permission_with_acting_user(acting_user, op).save(validate: false) || true
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
# if called from ServerDataCache then save and validate are both false, and we just return the
|
|
471
|
+
# vectors. ServerDataCache has its own transaction which it will rollback when its done
|
|
472
|
+
|
|
473
|
+
return vectors unless save || validate
|
|
474
|
+
|
|
475
|
+
# otherwise either save or validate or both are true, so we convert the remaining react_records into
|
|
476
|
+
# arrays with the id, model name, legal attributes, and any error messages. We also accumulate
|
|
477
|
+
# the all the error messages during a save so we can dump them to the server log.
|
|
478
|
+
|
|
479
|
+
all_messages = []
|
|
480
|
+
|
|
481
|
+
saved_models = reactive_records.collect do |reactive_record_id, model|
|
|
482
|
+
messages = model.errors.messages if validate && !model.valid?
|
|
483
|
+
all_messages << [model, messages] if save && messages
|
|
484
|
+
attributes = model.__hyperloop_secure_attributes(acting_user)
|
|
485
|
+
[reactive_record_id, model.class.name, attributes, messages]
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
# if we are not saving (i.e. just validating) then we rollback the transaction
|
|
489
|
+
|
|
490
|
+
raise ActiveRecord::Rollback, 'This Rollback is intentional!' unless save
|
|
491
|
+
|
|
492
|
+
# if there are error messages then we dump them to the server log, and raise an error
|
|
493
|
+
# to roll back the transaction and set success to false.
|
|
494
|
+
|
|
495
|
+
unless all_messages.empty?
|
|
496
|
+
::Rails.logger.debug "\033[0;31;1mERROR: HyperModel saving records failed:\033[0;30;21m"
|
|
497
|
+
all_messages.each do |model, message|
|
|
498
|
+
::Rails.logger.debug "\033[0;31;1m\t#{model}: #{message}\033[0;30;21m"
|
|
499
|
+
end
|
|
500
|
+
raise 'HyperModel saving records failed!'
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
{ success: true, saved_models: saved_models }
|
|
506
|
+
|
|
507
|
+
rescue Exception => e
|
|
508
|
+
if save || validate
|
|
509
|
+
{success: false, saved_models: saved_models, message: e}
|
|
510
|
+
else
|
|
511
|
+
{}
|
|
512
|
+
end
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
# destroy records
|
|
518
|
+
|
|
519
|
+
if RUBY_ENGINE == 'opal'
|
|
520
|
+
|
|
521
|
+
def destroy(&block)
|
|
522
|
+
|
|
523
|
+
return if @destroyed
|
|
524
|
+
|
|
525
|
+
#destroy_associations
|
|
526
|
+
|
|
527
|
+
promise = Promise.new
|
|
528
|
+
|
|
529
|
+
if !data_loading? and (id or vector)
|
|
530
|
+
Operations::Destroy.run(model: ar_instance.model_name.to_s, id: id, vector: vector)
|
|
531
|
+
.then do |response|
|
|
532
|
+
Broadcast.to_self ar_instance
|
|
533
|
+
yield response[:success], response[:message] if block
|
|
534
|
+
promise.resolve response
|
|
535
|
+
end
|
|
536
|
+
else
|
|
537
|
+
destroy_associations
|
|
538
|
+
# sync_unscoped_collection! # ? should we do this here was NOT being done before hypermesh integration
|
|
539
|
+
yield true, nil if block
|
|
540
|
+
promise.resolve({success: true})
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
# DO NOT CLEAR ATTRIBUTES. Records that are not found, are destroyed, and if they are searched for again, we want to make
|
|
544
|
+
# sure to find them. We may want to change this, and provide a separate flag called not_found. In this case you
|
|
545
|
+
# would put these lines here:
|
|
546
|
+
# @attributes = {}
|
|
547
|
+
# sync!
|
|
548
|
+
# and modify server_data_cache so that it does NOT call destroy
|
|
549
|
+
|
|
550
|
+
@destroyed = true
|
|
551
|
+
|
|
552
|
+
promise
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
else
|
|
556
|
+
|
|
557
|
+
def self.destroy_record(model, id, vector, acting_user)
|
|
558
|
+
model = Object.const_get(model)
|
|
559
|
+
record = if id
|
|
560
|
+
model.find(id)
|
|
561
|
+
else
|
|
562
|
+
ServerDataCache.new(acting_user, {})[*vector]
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
record.check_permission_with_acting_user(acting_user, :destroy_permitted?).destroy
|
|
567
|
+
{success: true, attributes: {}}
|
|
568
|
+
|
|
569
|
+
rescue Exception => e
|
|
570
|
+
#ReactiveRecord::Pry.rescued(e)
|
|
571
|
+
{success: false, record: record, message: e}
|
|
572
|
+
end
|
|
573
|
+
end
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
end
|