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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8f56e1c745c9154f2197accee8cbd76fb6b623fcf112f11caedc29d4dcc0ee7f
4
+ data.tar.gz: c74e55520edf0064cb02ff378a70e55145a0400c21c65976de466ec283446a60
5
+ SHA512:
6
+ metadata.gz: d2fc69f4ad34347ecae9d5eac09c00c3f023b86306b1ceaab8b9593521b74db02751bbc8cb611e842f1a8f7b02f5cff2f954db9f5f2f0755facaccdd64a220c5
7
+ data.tar.gz: 553518e63790f5e7bb2d8b57d24113c86a074d2d985aaf787ebb263f3b4e7409d22d136d2dada4e5a7c601a353c0588f2d5ef46854540cff3f62be786f3663d0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018 Jan Biedermann
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,109 @@
1
+ # hyper-resource
2
+
3
+ HyperResource is an affective way of moving data between your server and clients when using Hyperloop and Rails.
4
+
5
+ [![Reactivity Demo](http://img.youtube.com/vi/fPSpESBbeMQ/0.jpg)](http://www.youtube.com/watch?v=fPSpESBbeMQ "Reactivity Demo")
6
+
7
+ ## Motivation
8
+
9
+ + To co-exist with a resource based REST API
10
+ + To have ActiveRecord type Models shared by both the client and server code
11
+ + To be ORM/database agnostic (tested with ActiveRecord on Postgres and Neo4j.rb on Neo4j)
12
+ + To fit the 'Rails way' as far as possible (under the covers, HyperResource is a traditional REST API)
13
+ + To keep all Policy checking and authorisation logic in the Rails Controllers
14
+ + To allow a stages implementation
15
+
16
+ ## Staged implementation
17
+
18
+ HyperResource is designed to be implemented in stages and each stage delivers value in its own right, so the developer only needs to go as far as they like.
19
+
20
+ A record can be of any ORM but the ORM must implement:
21
+ ```ruby
22
+ record_class.find(id) # to get a record
23
+ record.id # a identifier
24
+ record.updated_at # a time stamp
25
+ record.destroyed? # to identify if its scheduled for destruction
26
+
27
+ # when using relations controller
28
+ record.touch # to update updated_at, identicating that something about that record changed
29
+ # for example it has been added to a relation
30
+ ```
31
+
32
+ ### Stage 1 - Wrap a REST API with Ruby classes to represent Models
33
+
34
+ The simplest implementation of HyperResource is a client side only wrapper of an existing REST API which treats each REST Resource as a Ruby class.
35
+
36
+ ```ruby
37
+ # in your client-cide code
38
+ class Customer
39
+ include ApplicationHyperRecord
40
+ end
41
+
42
+ # then work with the Customer class as if it were an ActiveRecord
43
+ customer = Customer.new(name: 'John Smith')
44
+ customer.save # ---> POST api/customer.json ... {name: 'John Smith' }
45
+ puts customer.id # 123
46
+
47
+ # to find a record
48
+ customer = Customer.find(123) # ---> GET api/customer/123.json
49
+ puts customer.name # `John Smith`
50
+ ```
51
+
52
+ ### Stage 2 - Adapt your Models so the client and server code share the same Models
53
+
54
+ HyperResource supports ActiveRecord associations and scopes so you can DRY up your code and the client an server can share the same Models.
55
+
56
+ ```ruby
57
+ module ApplicationHyperRecord
58
+ def self.included(base)
59
+ if RUBY_ENGINE == 'opal'
60
+ base.include(HyperRecord)
61
+ else
62
+ base.extend(HyperRecord::ServerClassMethods)
63
+ end
64
+ end
65
+ end
66
+
67
+ class Customer
68
+ include ApplicationHyperRecord
69
+ has_many :addresses
70
+
71
+ unless RUBY_ENGINE == 'opal'
72
+ # methods which should only exist on the server
73
+ end
74
+ end
75
+
76
+ customer = Customer.find(123) # ---> GET api/customer/123.json
77
+ customer.addresses.each do |address|
78
+ puts address.post_code
79
+ end
80
+ ```
81
+
82
+ ### Stage 3 - Implement a Redis based pub-sub mechanism so the client code is notified when the server data changes
83
+
84
+ ```ruby
85
+ class ApplicationController
86
+ include Hyperloop::Resource::PubSub
87
+
88
+ def my_action
89
+ # available methods for pubsub
90
+ publish_collection(base_record, collection_name, record = nil)
91
+ publish_record(record)
92
+ publish_scope(record_class, scope_name)
93
+
94
+ subscribe_collection(collection, base_record = nil, collection_name = nil)
95
+ subscribe_record(record)
96
+ subscribe_scope(collection, record_class = nil, scope_name = nil)
97
+
98
+ pub_sub_collection(collection, base_record, collection_name, causing_record = nil)
99
+ pub_sub_record(record)
100
+ pub_sub_scope(collection, record_class, scope_name)
101
+ end
102
+ end
103
+ ```
104
+
105
+ EXAMPLE
106
+
107
+ ## Implementation
108
+
109
+ How to install....
@@ -0,0 +1,30 @@
1
+ require_relative "lib/hyperloop/resource/version"
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "hyper-resource"
5
+ s.version = Hyperloop::Resource::VERSION
6
+ s.author = "Jan Biedermann"
7
+ s.email = "jan@kursator.de"
8
+ s.homepage = "https://github.com/janbiedermann/hyper-resource"
9
+ s.summary = "Transparent Opal Ruby Data/Resource Access from the browser for Ruby-Hyperloop"
10
+ s.description = "Write Browser Apps that transparently access server side resources like 'MyModel.first_name', with ease"
11
+
12
+ s.files = `git ls-files`.split("\n")
13
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
14
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
15
+ s.require_paths = ["lib"]
16
+
17
+ s.add_runtime_dependency "opal", "~> 0.11.0"
18
+ s.add_runtime_dependency "opal-activesupport", "~> 0.3.1"
19
+ s.add_runtime_dependency 'hyper-component' , '~> 1.0.0.lap27'
20
+ s.add_runtime_dependency 'hyper-store' , '~> 1.0.0.lap27'
21
+ s.add_runtime_dependency "hyperloop-config", "~> 1.0.0.lap27"
22
+ s.add_development_dependency "hyperloop", "~> 1.0.0.lap27"
23
+ s.add_development_dependency "hyper-spec", "~> 1.0.0.lap27"
24
+ s.add_development_dependency "listen"
25
+ s.add_development_dependency "rake", ">= 11.3.0"
26
+ s.add_development_dependency "rails", ">= 5.1.0"
27
+ s.add_development_dependency "redis"
28
+ s.add_development_dependency "rspec-rails"
29
+ s.add_development_dependency "sqlite3"
30
+ end
@@ -0,0 +1,20 @@
1
+ require 'hyperloop-config'
2
+ require 'opal-activesupport'
3
+ Hyperloop.import 'pusher/source/pusher.js', client_only: true
4
+ require 'hyperloop/resource/version'
5
+ require 'hyperloop/resource/client_drivers' # initialize options for the client
6
+ require 'hyperloop/resource/config'
7
+ require 'hyper-store'
8
+ Hyperloop.import 'hyper-resource'
9
+
10
+
11
+ if RUBY_ENGINE == 'opal'
12
+ require 'hyperloop/resource/http'
13
+ require 'hyper-store'
14
+ require 'hyper_record'
15
+ else
16
+ require 'hyperloop/resource/pub_sub' # server side, controller helper methods
17
+ require 'hyperloop/resource/security_guards' # server side, controller helper methods
18
+ require 'hyper_record'
19
+ Opal.append_path __dir__.untaint
20
+ end
@@ -0,0 +1,21 @@
1
+ if RUBY_ENGINE == 'opal'
2
+ require 'hyper_record/dummy_value'
3
+ require 'hyper_record/collection'
4
+ require 'hyper_record/class_methods'
5
+ require 'hyper_record/client_instance_methods'
6
+
7
+ module HyperRecord
8
+ def self.included(base)
9
+ base.include(Hyperloop::Store::Mixin)
10
+ base.extend(HyperRecord::ClassMethods)
11
+ base.include(HyperRecord::ClientInstanceMethods)
12
+ base.class_eval do
13
+ state :record_state
14
+ end
15
+ end
16
+ end
17
+ else
18
+ require 'hyper_record/server_class_methods'
19
+ end
20
+
21
+
@@ -0,0 +1,413 @@
1
+ module HyperRecord
2
+ module ClassMethods
3
+
4
+ def new(record_hash = {})
5
+ if record_hash.has_key?(:id)
6
+ record = _record_cache[record_hash[:id]]
7
+ if record
8
+ record.instance_variable_get(:@properties_hash).merge!(record_hash)
9
+ return record
10
+ end
11
+ end
12
+ super(record_hash)
13
+ end
14
+
15
+ def all
16
+ _register_class_observer
17
+ if _class_fetch_states[:all] == 'f'
18
+ record_collection = HyperRecord::Collection.new
19
+ _record_cache.each_value { |record| record_collection.push(record) }
20
+ return record_collection
21
+ end
22
+ _promise_get("#{resource_base_uri}.json").then do |response|
23
+ klass_name = self.to_s.underscore
24
+ klass_key = klass_name.pluralize
25
+ response.json[klass_key].each do |record_json|
26
+ self.new(record_json[klass_name])
27
+ end
28
+ _class_fetch_states[:all] = 'f'
29
+ _notify_class_observers
30
+ record_collection = HyperRecord::Collection.new
31
+ _record_cache.each_value { |record| record_collection.push(record) }
32
+ record_collection
33
+ end.fail do |response|
34
+ error_message = "#{self.to_s}.all failed to fetch records!"
35
+ `console.error(error_message)`
36
+ response
37
+ end
38
+ HyperRecord::Collection.new
39
+ end
40
+
41
+ def belongs_to(direction, name = nil, options = { type: nil })
42
+ if name.is_a?(Hash)
43
+ options.merge(name)
44
+ name = direction
45
+ direction = nil
46
+ elsif name.nil?
47
+ name = direction
48
+ end
49
+ reflections[name] = { direction: direction, type: options[:type], kind: :belongs_to }
50
+ define_method(name) do
51
+ _register_observer
52
+ if @fetch_states[name] == 'f'
53
+ @relations[name]
54
+ elsif self.id
55
+ self.class._promise_get("#{self.class.resource_base_uri}/#{self.id}/relations/#{name}.json").then do |response|
56
+ @relations[name] = self.class._convert_json_hash_to_record(response.json[self.class.to_s.underscore][name])
57
+ @fetch_states[name] = 'f'
58
+ _notify_observers
59
+ @relations[name]
60
+ end.fail do |response|
61
+ error_message = "#{self.class.to_s}[#{self.id}].#{name}, a has_one association, failed to fetch records!"
62
+ `console.error(error_message)`
63
+ response
64
+ end
65
+ @relations[name]
66
+ else
67
+ @relations[name]
68
+ end
69
+ end
70
+ define_method("#{name}=") do |arg|
71
+ _register_observer
72
+ @relations[name] = arg
73
+ @fetch_states[name] == 'f'
74
+ @relations[name]
75
+ end
76
+ end
77
+
78
+ def create(record_hash = {})
79
+ record = new(record_hash)
80
+ record.save
81
+ record
82
+ end
83
+
84
+ def find(id)
85
+ return _record_cache[id] if _record_cache.has_key?(id)
86
+
87
+ observer = React::State.current_observer
88
+ record_in_progress = self.new
89
+
90
+ record_in_progress_key = "#{self.to_s}_#{record_in_progress.object_id}"
91
+ React::State.get_state(observer, record_in_progress_key) if observer
92
+
93
+ _promise_get("#{resource_base_uri}/#{id}.json").then do |response|
94
+ klass_key = self.to_s.underscore
95
+ reflections.keys.each do |relation|
96
+ if response.json[klass_key].has_key?(relation)
97
+ response.json[klass_key][r_or_s] = _convert_array_to_collection(response.json[klass_key][relation])
98
+ record_in_progress.instance_variable_get(:@fetch_states)[relation] = 'f'
99
+ end
100
+ end
101
+ record_in_progress._initialize_from_hash(response.json[klass_key]) if response.json[klass_key]
102
+ _record_cache[record_in_progress.id] = record_in_progress
103
+ React::State.set_state(observer, record_in_progress_key, `Date.now() + Math.random()`) if observer
104
+ record_in_progress
105
+ end.fail do |response|
106
+ error_message = "#{self.to_s}.find(#{id}) failed to fetch record!"
107
+ `console.error(error_message)`
108
+ response
109
+ end
110
+
111
+ record_in_progress
112
+ end
113
+
114
+ def find_record(id)
115
+ # TODO this is bogus, needs some attention
116
+ _promise_get("#{resource_base_uri}/#{id}.json").then do |response|
117
+ klass_name = self.to_s.underscore
118
+ self.new(response.json[klass_name])
119
+ end
120
+ end
121
+
122
+ def find_record_by(hash)
123
+ return _record_cache[hash] if _record_cache.has_key?(hash)
124
+ # TODO needs clarification about how to call the endpoint
125
+ _promise_get("#{resource_base_uri}/#{id}.json").then do |reponse|
126
+ record = self.new(response.json[self.to_s.underscore])
127
+ _record_cache[hash] = record
128
+ end
129
+ end
130
+
131
+ def has_many(direction, name = nil, options = { type: nil })
132
+ if name.is_a?(Hash)
133
+ options.merge(name)
134
+ name = direction
135
+ direction = nil
136
+ elsif name.nil?
137
+ name = direction
138
+ end
139
+ reflections[name] = { direction: direction, type: options[:type], kind: :has_many }
140
+ define_method(name) do
141
+ _register_observer
142
+ if @fetch_states[name] == 'f'
143
+ @relations[name]
144
+ elsif self.id
145
+ self.class._promise_get("#{self.class.resource_base_uri}/#{self.id}/relations/#{name}.json").then do |response|
146
+ collection = self.class._convert_array_to_collection(response.json[self.class.to_s.underscore][name], self, name)
147
+ @relations[name] = collection
148
+ @fetch_states[name] = 'f'
149
+ _notify_observers
150
+ @relations[name]
151
+ end.fail do |response|
152
+ error_message = "#{self.class.to_s}[#{self.id}].#{name}, a has_many association, failed to fetch records!"
153
+ `console.error(error_message)`
154
+ response
155
+ end
156
+ @relations[name]
157
+ else
158
+ @relations[name]
159
+ end
160
+ end
161
+ define_method("#{name}=") do |arg|
162
+ _register_observer
163
+ collection = if arg.is_a?(Array)
164
+ HyperRecord::Collection.new(arg, self, name)
165
+ elsif arg.is_a?(HyperRecord::Collection)
166
+ arg
167
+ else
168
+ raise "Argument must be a HyperRecord::Collection or a Array"
169
+ end
170
+ @relations[name] = collection
171
+ @fetch_states[name] == 'f'
172
+ @relations[name]
173
+ end
174
+ end
175
+
176
+ def has_one(direction, name, options = { type: nil })
177
+ if name.is_a?(Hash)
178
+ options.merge(name)
179
+ name = direction
180
+ direction = nil
181
+ elsif name.nil?
182
+ name = direction
183
+ end
184
+ reflections[name] = { direction: direction, type: options[:type], kind: :has_one }
185
+ define_method(name) do
186
+ _register_observer
187
+ if @fetch_states[name] == 'f'
188
+ @relations[name]
189
+ elsif self.id
190
+ self.class._promise_get("#{self.class.resource_base_uri}/#{self.id}/relations/#{name}.json").then do |response|
191
+ @relations[name] = self.class._convert_json_hash_to_record(response.json[self.class.to_s.underscore][name])
192
+ @fetch_states[name] = 'f'
193
+ _notify_observers
194
+ @relations[name]
195
+ end.fail do |response|
196
+ error_message = "#{self.class.to_s}[#{self.id}].#{name}, a has_one association, failed to fetch records!"
197
+ `console.error(error_message)`
198
+ response
199
+ end
200
+ @relations[name]
201
+ else
202
+ @relations[name]
203
+ end
204
+ end
205
+ define_method("#{name}=") do |arg|
206
+ _register_observer
207
+ @relations[name] = arg
208
+ @fetch_states[name] == 'f'
209
+ @relations[name]
210
+ end
211
+ end
212
+
213
+ def record_cached?(id)
214
+ _record_cache.has_key?(id)
215
+ end
216
+
217
+ def property(name, options = {})
218
+ # ToDo options maybe, ddefault value? type check?
219
+ _property_options[name] = options
220
+ define_method(name) do
221
+ _register_observer
222
+ if @properties_hash[:id]
223
+ if @changed_properties_hash.has_key?(name)
224
+ @changed_properties_hash[name]
225
+ else
226
+ @properties_hash[name]
227
+ end
228
+ else
229
+ # record has not been fetched or is new and not yet saved
230
+ if @properties_hash[name].nil?
231
+ # TODO move default to initializer?
232
+ if self.class._property_options[name].has_key?(:default)
233
+ self.class._property_options[name][:default]
234
+ elsif self.class._property_options[name].has_key?(:type)
235
+ HyperRecord::DummyValue.new(self.class._property_options[name][:type])
236
+ else
237
+ HyperRecord::DummyValue.new(Nil)
238
+ end
239
+ else
240
+ @properties_hash[name]
241
+ end
242
+ end
243
+ end
244
+ define_method("#{name}=") do |value|
245
+ _register_observer
246
+ @changed_properties_hash[name] = value
247
+ end
248
+ end
249
+
250
+ def reflections
251
+ @reflections ||= {}
252
+ end
253
+
254
+ def rest_method(name, options = { default_result: '...' })
255
+ rest_methods[name] = options
256
+ define_method(name) do |*args|
257
+ _register_observer
258
+ if self.id && (@rest_methods_hash[name][:force] || !@rest_methods_hash[name].has_key?(:result))
259
+ self.class._rest_method_get_or_patch(name, self.id, *args).then do |result|
260
+ @rest_methods_hash[name][:result] = result # result is parsed json
261
+ _notify_observers
262
+ @rest_methods_hash[name][:result]
263
+ end.fail do |response|
264
+ error_message = "#{self.class.to_s}[#{self.id}].#{name}, a rest_method, failed to fetch records!"
265
+ `console.error(error_message)`
266
+ response
267
+ end
268
+ end
269
+ if @rest_methods_hash[name].has_key?(:result)
270
+ @rest_methods_hash[name][:result]
271
+ else
272
+ self.class.rest_methods[name][:default_result]
273
+ end
274
+ end
275
+ end
276
+
277
+ def rest_methods
278
+ @rest_methods_hash ||= {}
279
+ end
280
+
281
+ def resource_base_uri
282
+ @resource ||= "#{Hyperloop::Resource::ClientDrivers.opts[:resource_api_base_path]}/#{self.to_s.underscore.pluralize}"
283
+ end
284
+
285
+ def scope(name, options)
286
+ scopes[name] = HyperRecord::Collection.new
287
+ define_singleton_method(name) do
288
+ if _class_fetch_states[name] == 'f'
289
+ scopes[name]
290
+ else
291
+ _register_class_observer
292
+ self._promise_get("#{resource_base_uri}/scopes/#{name}.json").then do |response|
293
+ scopes[name] = _convert_array_to_collection(response.json[self.to_s.underscore][name])
294
+ _class_fetch_states[name] = 'f'
295
+ _notify_class_observers
296
+ scopes[name]
297
+ end.fail do |response|
298
+ error_message = "#{self.class.to_s}.#{name}, a scope, failed to fetch records!"
299
+ `console.error(error_message)`
300
+ response
301
+ end
302
+ scopes[name]
303
+ end
304
+ end
305
+ end
306
+
307
+ def scopes
308
+ @scopes ||= {}
309
+ end
310
+
311
+ ### internal
312
+
313
+ def _convert_array_to_collection(array, record = nil, relation_name = nil)
314
+ res = array.map do |record_hash|
315
+ _convert_json_hash_to_record(record_hash)
316
+ end
317
+ HyperRecord::Collection.new(res, record, relation_name)
318
+ end
319
+
320
+ def _convert_json_hash_to_record(record_hash)
321
+ return nil if !record_hash
322
+ klass_key = record_hash.keys.first
323
+ return nil if klass_key == "nil_class"
324
+ return nil if !record_hash[klass_key]
325
+ return nil if record_hash[klass_key].keys.size == 0
326
+ record_class = klass_key.camelize.constantize
327
+ if record_hash[klass_key][:id].nil?
328
+ record_class.new(record_hash[klass_key])
329
+ else
330
+ record = record_class._record_cache[record_hash[klass_key][:id]]
331
+ if record.nil?
332
+ record = record_class.new(record_hash[klass_key])
333
+ else
334
+ record._initialize_from_hash(record_hash[klass_key])
335
+ end
336
+ record
337
+ end
338
+ end
339
+
340
+ def _class_fetch_states
341
+ @_class_fetch_states ||= { all: 'n' }
342
+ @_class_fetch_states
343
+ end
344
+
345
+ def _class_observers
346
+ @_class_observers ||= Set.new
347
+ @_class_observers
348
+ end
349
+
350
+ def _class_state_key
351
+ @_class_state_key ||= self.to_s
352
+ @_class_state_key
353
+ end
354
+
355
+ def _notify_class_observers
356
+ _class_observers.each do |observer|
357
+ React::State.set_state(observer, _class_state_key, `Date.now() + Math.random()`)
358
+ end
359
+ _class_observers = Set.new
360
+ end
361
+
362
+ def _promise_get(uri)
363
+ Hyperloop::Resource::HTTP.get(uri, headers: { 'Content-Type' => 'application/json' })
364
+ end
365
+
366
+ def _promise_delete(uri)
367
+ Hyperloop::Resource::HTTP.delete(uri, headers: { 'Content-Type' => 'application/json' })
368
+ end
369
+
370
+ def _promise_patch(uri, payload)
371
+ Hyperloop::Resource::HTTP.patch(uri, payload: payload,
372
+ headers: { 'Content-Type' => 'application/json' },
373
+ dataType: :json)
374
+ end
375
+
376
+ def _promise_post(uri, payload)
377
+ Hyperloop::Resource::HTTP.post(uri, payload: payload,
378
+ headers: { 'Content-Type' => 'application/json' },
379
+ dataType: :json)
380
+ end
381
+
382
+ def _property_options
383
+ @property_options ||= {}
384
+ end
385
+
386
+ def _record_cache
387
+ @record_cache ||= {}
388
+ end
389
+
390
+ def _register_class_observer
391
+ observer = React::State.current_observer
392
+ if observer
393
+ React::State.get_state(observer, _class_state_key)
394
+ _class_observers << observer # @observers is a set, observers get added only once
395
+ end
396
+ end
397
+
398
+ def _rest_method_get_or_patch(name, id, *args)
399
+ uri = "#{resource_base_uri}/#{id}/methods/#{name}.json?timestamp=#{`Date.now() + Math.random()`}" # timestamp to invalidate browser caches
400
+ if args && args.size > 0
401
+ payload = { params: args }
402
+ _promise_patch(uri, payload).then do |result|
403
+ result.json[:result]
404
+ end
405
+ else
406
+ _promise_get(uri).then do |result|
407
+ result.json[:result]
408
+ end
409
+ end
410
+ end
411
+ end
412
+
413
+ end