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.
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