hyper-resource 1.0.0.lap34
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![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
|
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
|