hyper-resource 1.0.0.lap34
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +109 -0
- data/hyper-resource.gemspec +30 -0
- data/lib/hyper-resource.rb +20 -0
- data/lib/hyper_record.rb +21 -0
- data/lib/hyper_record/class_methods.rb +413 -0
- data/lib/hyper_record/client_instance_methods.rb +300 -0
- data/lib/hyper_record/collection.rb +39 -0
- data/lib/hyper_record/dummy_value.rb +23 -0
- data/lib/hyper_record/server_class_methods.rb +19 -0
- data/lib/hyperloop/resource/client_drivers.rb +109 -0
- data/lib/hyperloop/resource/config.rb +15 -0
- data/lib/hyperloop/resource/http.rb +310 -0
- data/lib/hyperloop/resource/pub_sub.rb +146 -0
- data/lib/hyperloop/resource/rails/controller_templates/methods_controller.rb +63 -0
- data/lib/hyperloop/resource/rails/controller_templates/properties_controller.rb +21 -0
- data/lib/hyperloop/resource/rails/controller_templates/relations_controller.rb +123 -0
- data/lib/hyperloop/resource/rails/controller_templates/scopes_controller.rb +37 -0
- data/lib/hyperloop/resource/security_guards.rb +37 -0
- data/lib/hyperloop/resource/version.rb +5 -0
- data/lib/pusher/source/pusher.js +4183 -0
- metadata +246 -0
@@ -0,0 +1,300 @@
|
|
1
|
+
module HyperRecord
|
2
|
+
module ClientInstanceMethods
|
3
|
+
|
4
|
+
def initialize(record_hash = {})
|
5
|
+
# initalize internal data structures
|
6
|
+
record_hash = {} if record_hash.nil?
|
7
|
+
@properties_hash = {}
|
8
|
+
@changed_properties_hash = {}
|
9
|
+
@relations = {}
|
10
|
+
@rest_methods_hash = {}
|
11
|
+
self.class.rest_methods.keys.each { |method| @rest_methods_hash[method] = {} }
|
12
|
+
@destroyed = false
|
13
|
+
# for reactivity
|
14
|
+
@fetch_states = {}
|
15
|
+
@state_key = "#{self.class.to_s}_#{self.object_id}"
|
16
|
+
@observers = Set.new
|
17
|
+
@registered_collections = Set.new
|
18
|
+
|
19
|
+
_initialize_from_hash(record_hash)
|
20
|
+
|
21
|
+
# for reactivity
|
22
|
+
_register_observer
|
23
|
+
end
|
24
|
+
|
25
|
+
def _initialize_from_hash(record_hash)
|
26
|
+
reflections.keys.each do |relation|
|
27
|
+
if record_hash.has_key?(relation)
|
28
|
+
@fetch_states[relation] = 'f' # fetched
|
29
|
+
if reflections[relation][:kind] == :has_many
|
30
|
+
if record_hash[relation].nil?
|
31
|
+
@relations[relation] = HyperRecord::Collection.new([], self, relation)
|
32
|
+
else
|
33
|
+
@relations[relation] = self.class._convert_array_to_collection(record_hash[relation], self, relation)
|
34
|
+
end
|
35
|
+
else
|
36
|
+
@relations[relation] = self.class._convert_json_hash_to_record(record_hash[relation])
|
37
|
+
end
|
38
|
+
else
|
39
|
+
unless @fetch_states[collection] == 'f'
|
40
|
+
if reflections[relation][:kind] == :has_many
|
41
|
+
@relations[relation] = HyperRecord::Collection.new([], self, relation)
|
42
|
+
else
|
43
|
+
@relations[relation] = nil
|
44
|
+
end
|
45
|
+
@fetch_states[relation] = 'n' # not fetched
|
46
|
+
end
|
47
|
+
end
|
48
|
+
record_hash.delete(relation)
|
49
|
+
end
|
50
|
+
|
51
|
+
@properties_hash = record_hash
|
52
|
+
|
53
|
+
# change state
|
54
|
+
_mutate_state
|
55
|
+
|
56
|
+
# cache in global cache
|
57
|
+
self.class._record_cache[@properties_hash[:id]] = self if @properties_hash.has_key?(:id)
|
58
|
+
end
|
59
|
+
|
60
|
+
### reactive api
|
61
|
+
|
62
|
+
def destroy
|
63
|
+
destroy_record
|
64
|
+
nil
|
65
|
+
end
|
66
|
+
|
67
|
+
def destroyed?
|
68
|
+
@destroyed
|
69
|
+
end
|
70
|
+
|
71
|
+
def link(other_record)
|
72
|
+
link_record(other_record)
|
73
|
+
self
|
74
|
+
end
|
75
|
+
|
76
|
+
def method_missing(method, arg)
|
77
|
+
if method.end_with?('=')
|
78
|
+
_register_observer
|
79
|
+
@changed_properties_hash[method.chop] = arg
|
80
|
+
else
|
81
|
+
_register_observer
|
82
|
+
if @changed_properties_hash.has_key?(method)
|
83
|
+
@changed_properties_hash[method]
|
84
|
+
else
|
85
|
+
@properties_hash[method]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def reflections
|
91
|
+
self.class.reflections
|
92
|
+
end
|
93
|
+
|
94
|
+
def reset
|
95
|
+
_register_observer
|
96
|
+
@changed_properties_hash = {}
|
97
|
+
end
|
98
|
+
|
99
|
+
def resource_base_uri
|
100
|
+
self.class.resource_base_uri
|
101
|
+
end
|
102
|
+
|
103
|
+
def rest_method_force_updates(method_name)
|
104
|
+
@rest_methods_hash[method_name][:force] = true
|
105
|
+
end
|
106
|
+
|
107
|
+
def rest_method_unforce_updates(method_name)
|
108
|
+
@rest_methods_hash[method_name][:force] = false
|
109
|
+
end
|
110
|
+
|
111
|
+
def save
|
112
|
+
save_record
|
113
|
+
self
|
114
|
+
end
|
115
|
+
|
116
|
+
def to_hash
|
117
|
+
_register_observer
|
118
|
+
res = @properties_hash.dup
|
119
|
+
res.merge!(@changed_properties_hash)
|
120
|
+
res
|
121
|
+
end
|
122
|
+
|
123
|
+
def to_s
|
124
|
+
_register_observer
|
125
|
+
@properties_hash.to_s
|
126
|
+
end
|
127
|
+
|
128
|
+
def unlink(other_record)
|
129
|
+
unlink_record(other_record, observer)
|
130
|
+
self
|
131
|
+
end
|
132
|
+
|
133
|
+
### promise api
|
134
|
+
|
135
|
+
def destroy_record
|
136
|
+
_local_destroy
|
137
|
+
self.class._promise_delete("#{resource_base_uri}/#{@properties_hash[:id]}").then do |response|
|
138
|
+
nil
|
139
|
+
end.fail do |response|
|
140
|
+
error_message = "Destroying record #{self} failed!"
|
141
|
+
`console.error(error_message)`
|
142
|
+
response
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def link_record(other_record, relation_name = nil)
|
147
|
+
_register_observer
|
148
|
+
called_from_collection = relation_name ? true : false
|
149
|
+
relation_name = other_record.class.to_s.underscore.pluralize unless relation_name
|
150
|
+
if reflections.has_key?(relation_name)
|
151
|
+
self.send(relation_name).push(other_record) unless called_from_collection
|
152
|
+
else
|
153
|
+
relation_name = other_record.class.to_s.underscore
|
154
|
+
raise "No collection for record of type #{other_record.class}" unless reflections.has_key?(relation_name)
|
155
|
+
self.send("#{relation_name}=", other_record) unless called_from_collection
|
156
|
+
end
|
157
|
+
payload_hash = other_record.to_hash
|
158
|
+
self.class._promise_post("#{resource_base_uri}/#{self.id}/relations/#{relation_name}.json", { data: payload_hash }).then do |response|
|
159
|
+
other_record.instance_variable_get(:@properties_hash).merge!(response.json[other_record.class.to_s.underscore])
|
160
|
+
_notify_observers
|
161
|
+
self
|
162
|
+
end.fail do |response|
|
163
|
+
error_message = "Linking record #{other_record} to #{self} failed!"
|
164
|
+
`console.error(error_message)`
|
165
|
+
response
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def save_record
|
170
|
+
_register_observer
|
171
|
+
payload_hash = @properties_hash.merge(@changed_properties_hash) # copy hash, becasue we need to delete some keys
|
172
|
+
(%i[id created_at updated_at] + reflections.keys).each do |key|
|
173
|
+
payload_hash.delete(key)
|
174
|
+
end
|
175
|
+
if @properties_hash[:id] && ! (@changed_properties_hash.has_key?(:id) && @changed_properties_hash[:id].nil?)
|
176
|
+
reset
|
177
|
+
self.class._promise_patch("#{resource_base_uri}/#{@properties_hash[:id]}", { data: payload_hash }).then do |response|
|
178
|
+
@properties_hash.merge!(response.json[self.class.to_s.underscore])
|
179
|
+
_notify_observers
|
180
|
+
self
|
181
|
+
end.fail do |response|
|
182
|
+
error_message = "Saving record #{self} failed!"
|
183
|
+
`console.error(error_message)`
|
184
|
+
response
|
185
|
+
end
|
186
|
+
else
|
187
|
+
reset
|
188
|
+
self.class._promise_post(resource_base_uri, { data: payload_hash }).then do |response|
|
189
|
+
@properties_hash.merge!(response.json[self.class.to_s.underscore])
|
190
|
+
_notify_observers
|
191
|
+
self
|
192
|
+
end.fail do |response|
|
193
|
+
error_message = "Creating record #{self} failed!"
|
194
|
+
`console.error(error_message)`
|
195
|
+
response
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def unlink_record(other_record, relation_name = nil)
|
201
|
+
_register_observer
|
202
|
+
called_from_collection = collection_name ? true : false
|
203
|
+
relation_name = other_record.class.to_s.underscore.pluralize unless relation_name
|
204
|
+
raise "No relation for record of type #{other_record.class}" unless reflections.has_key?(relation_name)
|
205
|
+
self.send(relation_name).delete_if { |cr| cr == other_record } unless called_from_collection
|
206
|
+
self.class._promise_delete("#{resource_base_uri}/#{@properties_hash[:id]}/relations/#{relation_name}.json?record_id=#{other_record.id}").then do |response|
|
207
|
+
_notify_observers
|
208
|
+
self
|
209
|
+
end.fail do |response|
|
210
|
+
error_message = "Unlinking #{other_record} from #{self} failed!"
|
211
|
+
`console.error(error_message)`
|
212
|
+
response
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
### internal
|
217
|
+
|
218
|
+
def _local_destroy
|
219
|
+
_register_observer
|
220
|
+
@destroyed = true
|
221
|
+
self.class._record_cache.delete(@properties_hash[:id])
|
222
|
+
@registered_collections.dup.each do |collection|
|
223
|
+
collection.delete(self)
|
224
|
+
end
|
225
|
+
@registered_collections = Set.new
|
226
|
+
_notify_observers
|
227
|
+
end
|
228
|
+
|
229
|
+
def _notify_observers
|
230
|
+
mutate.record_state(`Date.now() + Math.random()`)
|
231
|
+
@observers.each do |observer|
|
232
|
+
React::State.set_state(observer, @state_key, `Date.now() + Math.random()`)
|
233
|
+
end
|
234
|
+
@observers = Set.new
|
235
|
+
self.class._notify_class_observers
|
236
|
+
end
|
237
|
+
|
238
|
+
def _register_collection(collection)
|
239
|
+
@registered_collections << collection
|
240
|
+
end
|
241
|
+
|
242
|
+
def _register_observer
|
243
|
+
observer = React::State.current_observer
|
244
|
+
if observer
|
245
|
+
React::State.get_state(observer, @state_key)
|
246
|
+
@observers << observer # @observers is a set, observers get added only once
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def _unregister_collection(collection)
|
251
|
+
@registered_collections.delete(collection)
|
252
|
+
end
|
253
|
+
|
254
|
+
def _update_record(data)
|
255
|
+
if data.has_key?(:relation)
|
256
|
+
if data.has_key?(:cause)
|
257
|
+
# this creation of variables for things that could be done in one line
|
258
|
+
# are a workaround for safari, to get it updating correctly
|
259
|
+
klass_name = data[:cause][:record_type]
|
260
|
+
c_record_class = Object.const_get(klass_name)
|
261
|
+
if c_record_class._record_cache.has_key?(data[:cause][:id])
|
262
|
+
c_record = c_record_class.find(data[:cause][:id])
|
263
|
+
if `Date.parse(#{c_record.updated_at}) >= Date.parse(#{data[:cause][:updated_at]})`
|
264
|
+
if @fetch_states[data[:relation]] == 'f'
|
265
|
+
if send(data[:relation]).include?(c_record)
|
266
|
+
return
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
relation_fetch_state = @fetch_states[data[:relation]]
|
273
|
+
if relation_fetch_state == 'f'
|
274
|
+
@fetch_states[data[:relation]] = 'u'
|
275
|
+
send(data[:relation])
|
276
|
+
end
|
277
|
+
return
|
278
|
+
end
|
279
|
+
if data[:destroyed]
|
280
|
+
return if self.destroyed?
|
281
|
+
@remotely_destroyed = true
|
282
|
+
_local_destroy
|
283
|
+
return
|
284
|
+
end
|
285
|
+
if @properties_hash[:updated_at] && data[:updated_at]
|
286
|
+
return if `Date.parse(#{@properties_hash[:updated_at]}) >= Date.parse(#{data[:updated_at]})`
|
287
|
+
end
|
288
|
+
self.class._promise_get("#{resource_base_uri}/#{@properties_hash[:id]}.json").then do |response|
|
289
|
+
klass_key = self.class.to_s.underscore
|
290
|
+
self._initialize_from_hash(response.json[klass_key]) if response.json[klass_key]
|
291
|
+
_notify_observers
|
292
|
+
self
|
293
|
+
end.fail do |response|
|
294
|
+
error_message = "#{self} failed to update!"
|
295
|
+
`console.error(error_message)`
|
296
|
+
response
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module HyperRecord
|
2
|
+
class Collection < Array
|
3
|
+
def initialize(array, record = nil, relation_name = nil)
|
4
|
+
@record = record
|
5
|
+
@relation_name = relation_name
|
6
|
+
if array
|
7
|
+
array.each do |record|
|
8
|
+
record._register_collection(self)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
@record._notify_observers if @record
|
12
|
+
array ? super(array) : super
|
13
|
+
end
|
14
|
+
|
15
|
+
def <<(other_record)
|
16
|
+
if @record && @relation_name
|
17
|
+
@record.link_record(other_record, @relation_name)
|
18
|
+
end
|
19
|
+
other_record._register_collection(self)
|
20
|
+
@record._notify_observers if @record
|
21
|
+
super(other_record)
|
22
|
+
end
|
23
|
+
|
24
|
+
def delete(other_record)
|
25
|
+
if @record && @relation_name && !other_record.instance_variable_get(:@remotely_destroyed)
|
26
|
+
@record.unlink_record(other_record, @relation_name)
|
27
|
+
end
|
28
|
+
other_record._unregister_collection(self)
|
29
|
+
@record._notify_observers if @record
|
30
|
+
super(other_record)
|
31
|
+
end
|
32
|
+
|
33
|
+
def push(other_record)
|
34
|
+
other_record._register_collection(self)
|
35
|
+
@record._notify_observers if @record
|
36
|
+
super(other_record)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module HyperRecord
|
2
|
+
class DummyValue
|
3
|
+
def intialize(type_class)
|
4
|
+
@backing_value = type_class.new
|
5
|
+
end
|
6
|
+
|
7
|
+
def method_missing(name, *args)
|
8
|
+
if args
|
9
|
+
@backing_value.send(name, args)
|
10
|
+
else
|
11
|
+
@backing_value.send(name)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def acts_as_string?
|
16
|
+
true
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_s
|
20
|
+
"..."
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module HyperRecord
|
2
|
+
module ServerClassMethods
|
3
|
+
def rest_method(name, options = { default_result: '...' }, &block)
|
4
|
+
rest_methods[name] = options
|
5
|
+
rest_methods[name][:params] = block.arity
|
6
|
+
define_method(name) do |*args|
|
7
|
+
if args.size > 0
|
8
|
+
instance_exec(*args, &block)
|
9
|
+
else
|
10
|
+
instance_exec(&block)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def rest_methods
|
16
|
+
@rest_methods_hash ||= {}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module Hyperloop
|
2
|
+
module Resource
|
3
|
+
class ClientDrivers
|
4
|
+
include React::IsomorphicHelpers
|
5
|
+
|
6
|
+
class << self
|
7
|
+
attr_reader :opts
|
8
|
+
end
|
9
|
+
|
10
|
+
if RUBY_ENGINE != 'opal'
|
11
|
+
|
12
|
+
prerender_footer do |controller|
|
13
|
+
# next if Hyperloop.transport == :none
|
14
|
+
# if defined?(PusherFake)
|
15
|
+
# path = ::Rails.application.routes.routes.detect do |route|
|
16
|
+
# route.app == Hyperloop::Engine ||
|
17
|
+
# (route.app.respond_to?(:app) && route.app.app == Hyperloop::Engine)
|
18
|
+
# end.path.spec
|
19
|
+
# pusher_fake_js = PusherFake.javascript(
|
20
|
+
# auth: { headers: { 'X-CSRF-Token' => controller.send(:form_authenticity_token) } },
|
21
|
+
# authEndpoint: "#{path}/hyperloop-pusher-auth"
|
22
|
+
# )
|
23
|
+
# end
|
24
|
+
|
25
|
+
config_hash = {
|
26
|
+
resource_transport: Hyperloop.resource_transport,
|
27
|
+
resource_api_base_path: Hyperloop.resource_api_base_path,
|
28
|
+
session_id: controller.session.id,
|
29
|
+
current_user_id: (controller.current_user.id if controller.current_user),
|
30
|
+
form_authenticity_token: controller.send(:form_authenticity_token)
|
31
|
+
}
|
32
|
+
case Hyperloop.resource_transport
|
33
|
+
when :action_cable
|
34
|
+
when :pusher
|
35
|
+
config_hash[:pusher] = {
|
36
|
+
key: Hyperloop.pusher[:key],
|
37
|
+
cluster: Hyperloop.pusher[:cluster],
|
38
|
+
encrypted: Hyperloop.pusher[:encrypted],
|
39
|
+
client_logging: Hyperloop.pusher[:client_logging] ? true : false
|
40
|
+
}
|
41
|
+
when :pusher_fake
|
42
|
+
end
|
43
|
+
|
44
|
+
"<script type='text/javascript'>\n"\
|
45
|
+
"window.HyperloopOpts = #{config_hash.to_json};\n"\
|
46
|
+
"Opal.Hyperloop.$const_get('Resource').$const_get('ClientDrivers').$initialize_client_drivers_on_boot();\n"\
|
47
|
+
"</script>\n"
|
48
|
+
end
|
49
|
+
|
50
|
+
else
|
51
|
+
|
52
|
+
def self.initialize_client_drivers_on_boot
|
53
|
+
|
54
|
+
return if @initialized
|
55
|
+
|
56
|
+
@initialized = true
|
57
|
+
@opts = {}
|
58
|
+
|
59
|
+
if on_opal_client?
|
60
|
+
|
61
|
+
@opts = Hash.new(`window.HyperloopOpts`)
|
62
|
+
@opts[:hyper_record_update_channel] = "hyper-record-update-channel-#{@opts[:session_id]}"
|
63
|
+
case @opts[:resource_transport]
|
64
|
+
when :pusher
|
65
|
+
if @opts[:pusher][:client_logging] && `window.console && window.console.log`
|
66
|
+
`Pusher.log = function(message) {window.console.log(message);}`
|
67
|
+
end
|
68
|
+
|
69
|
+
h = nil
|
70
|
+
pusher_api = nil
|
71
|
+
%x{
|
72
|
+
h = {
|
73
|
+
encrypted: #{@opts[:pusher][:encrypted]},
|
74
|
+
cluster: #{@opts[:pusher][:cluster]}
|
75
|
+
|
76
|
+
};
|
77
|
+
pusher_api = new Pusher(#{@opts[:pusher][:key]}, h)
|
78
|
+
}
|
79
|
+
@opts[:pusher][:pusher_api] = pusher_api
|
80
|
+
@opts[:pusher][:channel] = pusher_api.JS.subscribe(@opts[:hyper_record_update_channel])
|
81
|
+
@opts[:pusher][:channel].JS.bind('update', `function(data){
|
82
|
+
return Opal.Hyperloop.$const_get('Resource').$const_get('ClientDrivers').$process_notification(Opal.Hash.$new(data));
|
83
|
+
}`)
|
84
|
+
|
85
|
+
when opts[:resource_transport] == :action_cable
|
86
|
+
opts[:action_cable_consumer] =
|
87
|
+
`ActionCable.createConsumer.apply(ActionCable, #{[*opts[:action_cable_consumer_url]]})`
|
88
|
+
Hyperloop.connect(*opts[:auto_connect])
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.process_notification(data)
|
94
|
+
record_class = Object.const_get(data[:record_type])
|
95
|
+
if data[:scope]
|
96
|
+
scope_fetch_state = record_class._class_fetch_states[data[:scope]]
|
97
|
+
if scope_fetch_state == 'f'
|
98
|
+
record_class._class_fetch_states[data[:scope]] = 'u'
|
99
|
+
record_class.send(data[:scope])
|
100
|
+
end
|
101
|
+
elsif record_class.record_cached?(data[:id])
|
102
|
+
record = record_class.find(data[:id])
|
103
|
+
record._update_record(data)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|