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
checksums.yaml
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
|
+
[](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
|
data/lib/hyper_record.rb
ADDED
@@ -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
|