hyper-model 0.6.0 → 0.99.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (140) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +35 -41
  3. data/.rspec +2 -0
  4. data/.travis.yml +33 -0
  5. data/CHANGELOG.md +34 -0
  6. data/DOCS.md +735 -0
  7. data/Gemfile +7 -0
  8. data/Gemfile.lock +298 -224
  9. data/{LICENSE → LICENSE.txt} +6 -6
  10. data/README.md +51 -2
  11. data/Rakefile +18 -0
  12. data/bin/console +14 -0
  13. data/bin/setup +7 -0
  14. data/codeship.database.yml +18 -0
  15. data/hyper-model.gemspec +62 -36
  16. data/lib/active_model_client_stubs.rb +16 -0
  17. data/lib/active_record_base.rb +331 -0
  18. data/{examples/chat-app/app/assets/images/.keep → lib/acts_as_string.rb} +0 -0
  19. data/lib/enumerable/pluck.rb +6 -0
  20. data/lib/hyper-model.rb +59 -8
  21. data/lib/hyper_model/version.rb +3 -0
  22. data/lib/hyper_react/input_tags.rb +47 -0
  23. data/lib/hyperloop/model/load.rb +1 -1
  24. data/lib/kernel/itself.rb +5 -0
  25. data/lib/object/tap.rb +7 -0
  26. data/lib/opal/equality_patches.rb +15 -0
  27. data/lib/opal/parse_patch.rb +14 -0
  28. data/lib/opal/set_patches.rb +8 -0
  29. data/lib/reactive_record/active_record/aggregations.rb +69 -0
  30. data/lib/reactive_record/active_record/associations.rb +118 -0
  31. data/lib/reactive_record/active_record/base.rb +10 -0
  32. data/lib/reactive_record/active_record/class_methods.rb +406 -0
  33. data/lib/reactive_record/active_record/error.rb +31 -0
  34. data/lib/reactive_record/active_record/errors.rb +374 -0
  35. data/lib/reactive_record/active_record/instance_methods.rb +187 -0
  36. data/lib/reactive_record/active_record/public_columns_hash.rb +44 -0
  37. data/lib/reactive_record/active_record/reactive_record/backing_record_inspector.rb +36 -0
  38. data/lib/reactive_record/active_record/reactive_record/base.rb +416 -0
  39. data/lib/reactive_record/active_record/reactive_record/collection.rb +558 -0
  40. data/lib/reactive_record/active_record/reactive_record/column_types.rb +75 -0
  41. data/lib/reactive_record/active_record/reactive_record/dummy_value.rb +236 -0
  42. data/lib/reactive_record/active_record/reactive_record/getters.rb +133 -0
  43. data/lib/reactive_record/active_record/reactive_record/isomorphic_base.rb +576 -0
  44. data/lib/reactive_record/active_record/reactive_record/lookup_tables.rb +54 -0
  45. data/lib/reactive_record/active_record/reactive_record/operations.rb +107 -0
  46. data/lib/reactive_record/active_record/reactive_record/scoped_collection.rb +62 -0
  47. data/lib/reactive_record/active_record/reactive_record/setters.rb +194 -0
  48. data/lib/reactive_record/active_record/reactive_record/unscoped_collection.rb +16 -0
  49. data/lib/reactive_record/active_record/reactive_record/while_loading.rb +343 -0
  50. data/lib/reactive_record/active_record_error.rb +4 -0
  51. data/lib/reactive_record/broadcast.rb +223 -0
  52. data/lib/reactive_record/engine.rb +11 -0
  53. data/lib/reactive_record/interval.rb +190 -0
  54. data/lib/reactive_record/permissions.rb +117 -0
  55. data/lib/reactive_record/pry.rb +13 -0
  56. data/lib/reactive_record/reactive_scope.rb +18 -0
  57. data/lib/reactive_record/scope_description.rb +121 -0
  58. data/lib/reactive_record/serializers.rb +7 -0
  59. data/lib/reactive_record/server_data_cache.rb +478 -0
  60. data/path_release_steps.md +9 -0
  61. metadata +399 -109
  62. data/CODE_OF_CONDUCT.md +0 -49
  63. data/examples/chat-app/.gitignore +0 -21
  64. data/examples/chat-app/Gemfile +0 -62
  65. data/examples/chat-app/Gemfile.lock +0 -309
  66. data/examples/chat-app/README.md +0 -3
  67. data/examples/chat-app/Rakefile +0 -6
  68. data/examples/chat-app/app/assets/config/manifest.js +0 -3
  69. data/examples/chat-app/app/assets/javascripts/application.js +0 -3
  70. data/examples/chat-app/app/assets/stylesheets/application.scss +0 -33
  71. data/examples/chat-app/app/controllers/application_controller.rb +0 -3
  72. data/examples/chat-app/app/controllers/home_controller.rb +0 -5
  73. data/examples/chat-app/app/hyperloop/components/app.rb +0 -12
  74. data/examples/chat-app/app/hyperloop/components/formatted_div.rb +0 -15
  75. data/examples/chat-app/app/hyperloop/components/input_box.rb +0 -26
  76. data/examples/chat-app/app/hyperloop/components/message.rb +0 -29
  77. data/examples/chat-app/app/hyperloop/components/messages.rb +0 -8
  78. data/examples/chat-app/app/hyperloop/components/nav.rb +0 -30
  79. data/examples/chat-app/app/hyperloop/models/application_record.rb +0 -3
  80. data/examples/chat-app/app/hyperloop/models/message.rb +0 -6
  81. data/examples/chat-app/app/hyperloop/operations/operations.rb +0 -13
  82. data/examples/chat-app/app/hyperloop/stores/message_store.rb +0 -17
  83. data/examples/chat-app/app/policies/application_policy.rb +0 -9
  84. data/examples/chat-app/app/views/layouts/application.html.erb +0 -12
  85. data/examples/chat-app/bin/bundle +0 -3
  86. data/examples/chat-app/bin/rails +0 -9
  87. data/examples/chat-app/bin/rake +0 -9
  88. data/examples/chat-app/bin/setup +0 -34
  89. data/examples/chat-app/bin/spring +0 -17
  90. data/examples/chat-app/bin/update +0 -29
  91. data/examples/chat-app/config.ru +0 -5
  92. data/examples/chat-app/config/application.rb +0 -12
  93. data/examples/chat-app/config/boot.rb +0 -3
  94. data/examples/chat-app/config/cable.yml +0 -9
  95. data/examples/chat-app/config/database.yml +0 -25
  96. data/examples/chat-app/config/environment.rb +0 -5
  97. data/examples/chat-app/config/environments/development.rb +0 -56
  98. data/examples/chat-app/config/environments/production.rb +0 -86
  99. data/examples/chat-app/config/environments/test.rb +0 -42
  100. data/examples/chat-app/config/initializers/application_controller_renderer.rb +0 -6
  101. data/examples/chat-app/config/initializers/assets.rb +0 -11
  102. data/examples/chat-app/config/initializers/backtrace_silencers.rb +0 -7
  103. data/examples/chat-app/config/initializers/cookies_serializer.rb +0 -5
  104. data/examples/chat-app/config/initializers/filter_parameter_logging.rb +0 -4
  105. data/examples/chat-app/config/initializers/hyperloop.rb +0 -6
  106. data/examples/chat-app/config/initializers/inflections.rb +0 -16
  107. data/examples/chat-app/config/initializers/mime_types.rb +0 -4
  108. data/examples/chat-app/config/initializers/new_framework_defaults.rb +0 -24
  109. data/examples/chat-app/config/initializers/session_store.rb +0 -3
  110. data/examples/chat-app/config/initializers/wrap_parameters.rb +0 -14
  111. data/examples/chat-app/config/locales/en.yml +0 -23
  112. data/examples/chat-app/config/puma.rb +0 -47
  113. data/examples/chat-app/config/routes.rb +0 -5
  114. data/examples/chat-app/config/secrets.yml +0 -22
  115. data/examples/chat-app/config/spring.rb +0 -6
  116. data/examples/chat-app/db/migrate/20170319194429_create_message.rb +0 -9
  117. data/examples/chat-app/db/schema.rb +0 -48
  118. data/examples/chat-app/db/seeds.rb +0 -7
  119. data/examples/chat-app/lib/assets/.keep +0 -0
  120. data/examples/chat-app/lib/tasks/.keep +0 -0
  121. data/examples/chat-app/log/.keep +0 -0
  122. data/examples/chat-app/public/404.html +0 -67
  123. data/examples/chat-app/public/422.html +0 -67
  124. data/examples/chat-app/public/500.html +0 -66
  125. data/examples/chat-app/public/apple-touch-icon-precomposed.png +0 -0
  126. data/examples/chat-app/public/apple-touch-icon.png +0 -0
  127. data/examples/chat-app/public/favicon.ico +0 -0
  128. data/examples/chat-app/public/robots.txt +0 -5
  129. data/examples/chat-app/test/controllers/.keep +0 -0
  130. data/examples/chat-app/test/fixtures/.keep +0 -0
  131. data/examples/chat-app/test/fixtures/files/.keep +0 -0
  132. data/examples/chat-app/test/helpers/.keep +0 -0
  133. data/examples/chat-app/test/integration/.keep +0 -0
  134. data/examples/chat-app/test/mailers/.keep +0 -0
  135. data/examples/chat-app/test/models/.keep +0 -0
  136. data/examples/chat-app/test/test_helper.rb +0 -10
  137. data/examples/chat-app/tmp/.keep +0 -0
  138. data/examples/chat-app/vendor/assets/javascripts/.keep +0 -0
  139. data/examples/chat-app/vendor/assets/stylesheets/.keep +0 -0
  140. 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