graphiti 1.0.rc.2 → 1.0.rc.3
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 +4 -4
- data/graphiti.gemspec +1 -0
- data/lib/graphiti.rb +4 -0
- data/lib/graphiti/adapters/graphiti_api.rb +89 -0
- data/lib/graphiti/debugger.rb +3 -1
- data/lib/graphiti/errors.rb +42 -0
- data/lib/graphiti/query.rb +37 -4
- data/lib/graphiti/resource.rb +4 -0
- data/lib/graphiti/resource/configuration.rb +7 -0
- data/lib/graphiti/resource/persistence.rb +1 -1
- data/lib/graphiti/resource/remote.rb +68 -0
- data/lib/graphiti/resource/sideloading.rb +1 -0
- data/lib/graphiti/resource_proxy.rb +11 -6
- data/lib/graphiti/schema.rb +14 -1
- data/lib/graphiti/scope.rb +11 -5
- data/lib/graphiti/sideload.rb +52 -2
- data/lib/graphiti/sideload/belongs_to.rb +11 -1
- data/lib/graphiti/sideload/has_many.rb +11 -1
- data/lib/graphiti/util/persistence.rb +3 -0
- data/lib/graphiti/util/remote_params.rb +115 -0
- data/lib/graphiti/util/remote_serializer.rb +53 -0
- data/lib/graphiti/version.rb +1 -1
- metadata +20 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 673ff2ee2d99cc9cb04d8cfb86b5b2b03839b227
|
4
|
+
data.tar.gz: a07ef309b47401d52a75f9bdbd6675d09fd83117
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2f8911140b4fe3eb64420469e5ea428a7f7b9dc613d5eb5cf0acfa2169b4e20191e5be00f9c526770b8a05aa7de37d6dc5a84551bd0c4784532d6e7dde4c2d33
|
7
|
+
data.tar.gz: b920e16d339cbbd8c2b7f19fe539c02cf1ae8426b83fe9125854132474f46163075f945f4e29416b86febcf6458ac24828f3a956db7c8dc5cc9a8b39a7740362
|
data/graphiti.gemspec
CHANGED
@@ -25,6 +25,7 @@ Gem::Specification.new do |spec|
|
|
25
25
|
spec.add_dependency 'concurrent-ruby', '~> 1.0'
|
26
26
|
spec.add_dependency 'activesupport', ['>= 4.1', '< 6']
|
27
27
|
|
28
|
+
spec.add_development_dependency "faraday", '~> 0.15'
|
28
29
|
spec.add_development_dependency "activerecord", ['>= 4.1', '< 6']
|
29
30
|
spec.add_development_dependency "kaminari", '~> 0.17'
|
30
31
|
spec.add_development_dependency "bundler"
|
data/lib/graphiti.rb
CHANGED
@@ -29,6 +29,7 @@ require "graphiti/resource/interface"
|
|
29
29
|
require "graphiti/resource/polymorphism"
|
30
30
|
require "graphiti/resource/documentation"
|
31
31
|
require "graphiti/resource/persistence"
|
32
|
+
require "graphiti/resource/remote"
|
32
33
|
require "graphiti/sideload"
|
33
34
|
require "graphiti/sideload/has_many"
|
34
35
|
require "graphiti/sideload/belongs_to"
|
@@ -65,7 +66,10 @@ require "graphiti/util/serializer_attributes"
|
|
65
66
|
require "graphiti/util/serializer_relationships"
|
66
67
|
require "graphiti/util/class"
|
67
68
|
require "graphiti/util/link"
|
69
|
+
require "graphiti/util/remote_serializer"
|
70
|
+
require "graphiti/util/remote_params"
|
68
71
|
require 'graphiti/adapters/null'
|
72
|
+
require 'graphiti/adapters/graphiti_api'
|
69
73
|
require "graphiti/extensions/extra_attribute"
|
70
74
|
require "graphiti/extensions/boolean_attribute"
|
71
75
|
require "graphiti/extensions/temp_id"
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module Graphiti
|
2
|
+
module Adapters
|
3
|
+
class GraphitiAPI < ::Graphiti::Adapters::Null
|
4
|
+
def base_scope(model)
|
5
|
+
{}
|
6
|
+
end
|
7
|
+
|
8
|
+
def resolve(scope)
|
9
|
+
url = build_url(scope)
|
10
|
+
response = resource.make_request(url)
|
11
|
+
json = JSON.parse(response.body)
|
12
|
+
|
13
|
+
if json['errors']
|
14
|
+
handle_remote_error(url, json)
|
15
|
+
else
|
16
|
+
models = json['data'].map { |d| build_entity(json, d) }
|
17
|
+
Util::RemoteSerializer.for(resource.class.serializer, models)
|
18
|
+
models
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def handle_remote_error(url, json)
|
25
|
+
errors = json['errors'].map do |error|
|
26
|
+
if raw = error['meta'].try(:[], '__raw_error__')
|
27
|
+
{ message: raw['message'], backtrace: raw['backtrace'] }
|
28
|
+
else
|
29
|
+
{ message: "#{error['title']} - #{error['detail']}" }
|
30
|
+
end
|
31
|
+
end.compact
|
32
|
+
raise Errors::Remote.new(url, errors)
|
33
|
+
end
|
34
|
+
|
35
|
+
def build_url(scope)
|
36
|
+
url = resource.remote_url
|
37
|
+
params = scope[:params].merge(scope.except(:params))
|
38
|
+
params = CGI.unescape(params.to_query)
|
39
|
+
url = "#{url}?#{params}" unless params.blank?
|
40
|
+
url
|
41
|
+
end
|
42
|
+
|
43
|
+
def find_entity(json, id, type)
|
44
|
+
lookup = Array(json['data']) | Array(json['included'])
|
45
|
+
lookup.find { |l| l['id'] == id.to_s && l['type'] == type }
|
46
|
+
end
|
47
|
+
|
48
|
+
def build_entity(json, node)
|
49
|
+
entity = OpenStruct.new(node['attributes'])
|
50
|
+
entity.id = node['id']
|
51
|
+
entity._type = node['type']
|
52
|
+
process_relationships(entity, json, node['relationships'] || {})
|
53
|
+
entity
|
54
|
+
end
|
55
|
+
|
56
|
+
def process_relationships(entity, json, relationship_json)
|
57
|
+
entity._relationships = {}
|
58
|
+
relationship_json.each_pair do |name, hash|
|
59
|
+
if data = hash['data']
|
60
|
+
if data.is_a?(Array)
|
61
|
+
data.each do |d|
|
62
|
+
rel = find_entity(json, d['id'], d['type'])
|
63
|
+
related_entity = build_entity(json, rel)
|
64
|
+
add_relationship(entity, related_entity, name, true)
|
65
|
+
end
|
66
|
+
else
|
67
|
+
rel = find_entity(json, hash['data']['id'], hash['data']['type'])
|
68
|
+
related_entity = build_entity(json, rel)
|
69
|
+
add_relationship(entity, related_entity, name)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
Util::RemoteSerializer.for(Graphiti::Serializer, Array(entity[name]))
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def add_relationship(entity, related_entity, name, many = false)
|
77
|
+
if many
|
78
|
+
entity[name] ||= []
|
79
|
+
entity[name] << related_entity
|
80
|
+
entity._relationships[name] ||= []
|
81
|
+
entity._relationships[name] << related_entity
|
82
|
+
else
|
83
|
+
entity[name] = related_entity
|
84
|
+
entity._relationships[name] = related_entity
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
data/lib/graphiti/debugger.rb
CHANGED
@@ -17,7 +17,9 @@ module Graphiti
|
|
17
17
|
on_data_exception(payload, params)
|
18
18
|
else
|
19
19
|
if payload[:sideload]
|
20
|
-
|
20
|
+
if payload[:results]
|
21
|
+
on_sideload_data(payload, params, took)
|
22
|
+
end
|
21
23
|
else
|
22
24
|
on_primary_data(payload, params, took)
|
23
25
|
end
|
data/lib/graphiti/errors.rb
CHANGED
@@ -16,6 +16,36 @@ The adapter #{@adapter.class} does not implement method '#{@method}', which was
|
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
19
|
+
class SideloadConfig < Base
|
20
|
+
def initialize(name, parent_resource_class, message)
|
21
|
+
@name = name
|
22
|
+
@parent_resource_class = parent_resource_class
|
23
|
+
@message = message
|
24
|
+
end
|
25
|
+
|
26
|
+
def message
|
27
|
+
<<-MSG
|
28
|
+
#{@parent_resource_class} sideload :#{@name} - #{@message}
|
29
|
+
MSG
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class Remote < Base
|
34
|
+
def initialize(url, errors)
|
35
|
+
@url = url
|
36
|
+
@errors = errors
|
37
|
+
end
|
38
|
+
|
39
|
+
def message
|
40
|
+
msg = "Error hitting remote API: #{@url}"
|
41
|
+
@errors.each do |e|
|
42
|
+
msg << "\n\n#{e[:message]}"
|
43
|
+
msg << "\n\n#{e[:backtrace].join("\n")}\n\n\n\n" if e[:backtrace]
|
44
|
+
end
|
45
|
+
msg
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
19
49
|
class AroundCallbackProc < Base
|
20
50
|
def initialize(resource_class, method_name)
|
21
51
|
@resource_class = resource_class
|
@@ -29,6 +59,18 @@ The adapter #{@adapter.class} does not implement method '#{@method}', which was
|
|
29
59
|
end
|
30
60
|
end
|
31
61
|
|
62
|
+
class RemoteWrite < Base
|
63
|
+
def initialize(resource_class)
|
64
|
+
@resource_class = resource_class
|
65
|
+
end
|
66
|
+
|
67
|
+
def message
|
68
|
+
<<-MSG
|
69
|
+
#{@resource_class}: Tried to perform write operation. Writes are not supported for remote resources - hit the endpoint directly.
|
70
|
+
MSG
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
32
74
|
class UnsupportedOperator < Base
|
33
75
|
def initialize(resource, filter_name, supported, operator)
|
34
76
|
@resource = resource
|
data/lib/graphiti/query.rb
CHANGED
@@ -64,15 +64,27 @@ module Graphiti
|
|
64
64
|
end
|
65
65
|
end
|
66
66
|
|
67
|
+
def resource_for_sideload(sideload)
|
68
|
+
if @resource.remote?
|
69
|
+
Class.new(Graphiti::Resource) do
|
70
|
+
self.remote = '_remote_sideload_'
|
71
|
+
end.new
|
72
|
+
else
|
73
|
+
sideload.resource
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
67
77
|
def sideloads
|
68
78
|
@sideloads ||= begin
|
69
79
|
{}.tap do |hash|
|
70
80
|
include_hash.each_pair do |key, sub_hash|
|
71
81
|
sideload = @resource.class.sideload(key)
|
72
|
-
|
82
|
+
|
83
|
+
if sideload || @resource.remote?
|
84
|
+
sl_resource = resource_for_sideload(sideload)
|
73
85
|
_parents = parents + [self]
|
74
86
|
sub_hash = sub_hash[:include] if sub_hash.has_key?(:include)
|
75
|
-
hash[key] = Query.new(
|
87
|
+
hash[key] = Query.new(sl_resource, @params, key, sub_hash, _parents)
|
76
88
|
else
|
77
89
|
handle_missing_sideload(key)
|
78
90
|
end
|
@@ -131,7 +143,9 @@ module Graphiti
|
|
131
143
|
[].tap do |arr|
|
132
144
|
sort_hashes do |key, value, type|
|
133
145
|
if legacy_nested?(type)
|
134
|
-
@resource.
|
146
|
+
unless @resource.remote?
|
147
|
+
@resource.get_attr!(key, :sortable, request: true)
|
148
|
+
end
|
135
149
|
arr << { key => value }
|
136
150
|
elsif !type && top_level? && validate!(key, :sortable)
|
137
151
|
arr << { key => value }
|
@@ -196,6 +210,24 @@ module Graphiti
|
|
196
210
|
not [false, 'false'].include?(@params[:paginate])
|
197
211
|
end
|
198
212
|
|
213
|
+
# If this is a remote call, we don't care about local parents
|
214
|
+
def chain
|
215
|
+
if @resource.remote
|
216
|
+
top_remote_parent = parents.find { |p| p.resource.remote? }
|
217
|
+
[].tap do |chain|
|
218
|
+
parents.each do |p|
|
219
|
+
chain << p.association_name unless p == top_remote_parent
|
220
|
+
end
|
221
|
+
immediate_parent = parents.reverse[0]
|
222
|
+
# This is not currently checking that it is a remote of the same API
|
223
|
+
chain << association_name if immediate_parent && immediate_parent.resource.remote
|
224
|
+
chain.compact
|
225
|
+
end.compact
|
226
|
+
else
|
227
|
+
parents.map(&:association_name).compact + [association_name].compact
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
199
231
|
private
|
200
232
|
|
201
233
|
# Try to find on this resource
|
@@ -204,6 +236,7 @@ module Graphiti
|
|
204
236
|
# TODO: Eventually, remove the legacy logic
|
205
237
|
def validate!(name, flag)
|
206
238
|
return false if name.to_s.include?('.') # nested
|
239
|
+
return true if @resource.remote?
|
207
240
|
|
208
241
|
if att = @resource.get_attr(name, flag, request: true)
|
209
242
|
return att
|
@@ -248,7 +281,7 @@ module Graphiti
|
|
248
281
|
end
|
249
282
|
|
250
283
|
def handle_missing_sideload(name)
|
251
|
-
if Graphiti.config.raise_on_missing_sideload
|
284
|
+
if Graphiti.config.raise_on_missing_sideload && !@resource.remote?
|
252
285
|
raise Graphiti::Errors::InvalidInclude
|
253
286
|
.new(@resource, name)
|
254
287
|
end
|
data/lib/graphiti/resource.rb
CHANGED
@@ -35,6 +35,11 @@ module Graphiti
|
|
35
35
|
stat total: [:count]
|
36
36
|
end
|
37
37
|
|
38
|
+
def remote=(val)
|
39
|
+
super
|
40
|
+
include ::Graphiti::Resource::Remote
|
41
|
+
end
|
42
|
+
|
38
43
|
def model
|
39
44
|
klass = super
|
40
45
|
unless klass || abstract_class?
|
@@ -55,6 +60,8 @@ module Graphiti
|
|
55
60
|
|
56
61
|
class_attribute :adapter, instance_reader: false
|
57
62
|
class_attribute :model,
|
63
|
+
:remote,
|
64
|
+
:remote_base_url,
|
58
65
|
:type,
|
59
66
|
:polymorphic,
|
60
67
|
:polymorphic_child,
|
@@ -65,7 +65,7 @@ module Graphiti
|
|
65
65
|
def add_callback(kind, lifecycle, method = nil, only, &blk)
|
66
66
|
config[:callbacks][kind] ||= {}
|
67
67
|
config[:callbacks][kind][lifecycle] ||= []
|
68
|
-
config[:callbacks][kind][lifecycle] << { callback: (method || blk), only: only }
|
68
|
+
config[:callbacks][kind][lifecycle] << { callback: (method || blk), only: Array(only) }
|
69
69
|
end
|
70
70
|
end
|
71
71
|
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Graphiti
|
2
|
+
class Resource
|
3
|
+
module Remote
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
self.adapter = Graphiti::Adapters::GraphitiAPI
|
8
|
+
self.model = OpenStruct
|
9
|
+
self.validate_endpoints = false
|
10
|
+
|
11
|
+
class_attribute :timeout,
|
12
|
+
:open_timeout
|
13
|
+
end
|
14
|
+
|
15
|
+
class_methods do
|
16
|
+
def remote_url
|
17
|
+
[remote_base_url, remote].join
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def save(*args)
|
22
|
+
raise Errors::RemoteWrite.new(self.class)
|
23
|
+
end
|
24
|
+
|
25
|
+
def destroy(*args)
|
26
|
+
raise Errors::RemoteWrite.new(self.class)
|
27
|
+
end
|
28
|
+
|
29
|
+
def before_resolve(scope, query)
|
30
|
+
scope[:params] = Util::RemoteParams.generate(self, query)
|
31
|
+
scope
|
32
|
+
end
|
33
|
+
|
34
|
+
# Forward all headers
|
35
|
+
def request_headers
|
36
|
+
if defined?(Rails)
|
37
|
+
context.request.headers.to_h.reject { |k, v| k.include?('.') }
|
38
|
+
else
|
39
|
+
{}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def remote_url
|
44
|
+
self.class.remote_url
|
45
|
+
end
|
46
|
+
|
47
|
+
def make_request(url)
|
48
|
+
headers = request_headers.dup
|
49
|
+
headers['Content-Type'] = 'application/vnd.api+json'
|
50
|
+
faraday.get(url, nil, headers) do |req|
|
51
|
+
yield req if block_given? # for super do ... end
|
52
|
+
req.options.timeout = timeout if timeout
|
53
|
+
req.options.open_timeout = open_timeout if open_timeout
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def faraday
|
60
|
+
if defined?(Faraday)
|
61
|
+
Faraday
|
62
|
+
else
|
63
|
+
raise "Faraday not defined. Please require the 'faraday' gem to use remote resources"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -83,12 +83,17 @@ module Graphiti
|
|
83
83
|
def save(action: :create)
|
84
84
|
# TODO: remove this. Only used for persisting many-to-many with AR
|
85
85
|
# (see activerecord adapter)
|
86
|
-
Graphiti.context[:namespace]
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
@
|
91
|
-
|
86
|
+
original = Graphiti.context[:namespace]
|
87
|
+
begin
|
88
|
+
Graphiti.context[:namespace] = action
|
89
|
+
validator = persist do
|
90
|
+
@resource.persist_with_relationships \
|
91
|
+
@payload.meta(action: action),
|
92
|
+
@payload.attributes,
|
93
|
+
@payload.relationships
|
94
|
+
end
|
95
|
+
ensure
|
96
|
+
Graphiti.context[:namespace] = original
|
92
97
|
end
|
93
98
|
@data, success = validator.to_a
|
94
99
|
|
data/lib/graphiti/schema.rb
CHANGED
@@ -23,6 +23,8 @@ module Graphiti
|
|
23
23
|
|
24
24
|
def initialize(resources)
|
25
25
|
@resources = resources.sort_by(&:name)
|
26
|
+
@remote_resources = resources.select(&:remote?)
|
27
|
+
@resources = @resources - @remote_resources
|
26
28
|
end
|
27
29
|
|
28
30
|
def generate
|
@@ -79,7 +81,7 @@ module Graphiti
|
|
79
81
|
end
|
80
82
|
|
81
83
|
def generate_resources
|
82
|
-
@resources.map do |r|
|
84
|
+
arr = @resources.map do |r|
|
83
85
|
config = {
|
84
86
|
name: r.name,
|
85
87
|
type: r.type.to_s,
|
@@ -108,6 +110,17 @@ module Graphiti
|
|
108
110
|
|
109
111
|
config
|
110
112
|
end
|
113
|
+
|
114
|
+
arr |= @remote_resources.map do |r|
|
115
|
+
{
|
116
|
+
name: r.name,
|
117
|
+
description: r.description,
|
118
|
+
remote: r.remote_url,
|
119
|
+
relationships: relationships(r)
|
120
|
+
}
|
121
|
+
end
|
122
|
+
|
123
|
+
arr
|
111
124
|
end
|
112
125
|
|
113
126
|
def attributes(resource)
|
data/lib/graphiti/scope.rb
CHANGED
@@ -18,6 +18,7 @@ module Graphiti
|
|
18
18
|
[]
|
19
19
|
else
|
20
20
|
resolved = broadcast_data do |payload|
|
21
|
+
@object = @resource.before_resolve(@object, @query)
|
21
22
|
payload[:results] = @resource.resolve(@object)
|
22
23
|
payload[:results]
|
23
24
|
end
|
@@ -40,6 +41,7 @@ module Graphiti
|
|
40
41
|
|
41
42
|
@query.sideloads.each_pair do |name, q|
|
42
43
|
sideload = @resource.class.sideload(name)
|
44
|
+
next if sideload.nil? || sideload.shared_remote?
|
43
45
|
_parent = @resource
|
44
46
|
_context = Graphiti.context
|
45
47
|
resolve_sideload = -> {
|
@@ -94,11 +96,15 @@ module Graphiti
|
|
94
96
|
|
95
97
|
def apply_scoping(scope, opts)
|
96
98
|
@object = scope
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
99
|
+
|
100
|
+
unless @resource.remote?
|
101
|
+
opts[:default_paginate] = false unless @query.paginate?
|
102
|
+
add_scoping(nil, Graphiti::Scoping::DefaultFilter, opts)
|
103
|
+
add_scoping(:filter, Graphiti::Scoping::Filter, opts)
|
104
|
+
add_scoping(:sort, Graphiti::Scoping::Sort, opts)
|
105
|
+
add_scoping(:paginate, Graphiti::Scoping::Paginate, opts)
|
106
|
+
end
|
107
|
+
|
102
108
|
@object
|
103
109
|
end
|
104
110
|
|
data/lib/graphiti/sideload.rb
CHANGED
@@ -11,7 +11,8 @@ module Graphiti
|
|
11
11
|
:parent,
|
12
12
|
:group_name,
|
13
13
|
:link,
|
14
|
-
:description
|
14
|
+
:description,
|
15
|
+
:polymorphic_as
|
15
16
|
|
16
17
|
class_attribute :scope_proc,
|
17
18
|
:assign_proc,
|
@@ -22,6 +23,7 @@ module Graphiti
|
|
22
23
|
|
23
24
|
def initialize(name, opts)
|
24
25
|
@name = name
|
26
|
+
validate_options!(opts)
|
25
27
|
@parent_resource_class = opts[:parent_resource]
|
26
28
|
@resource_class = opts[:resource]
|
27
29
|
@primary_key = opts[:primary_key]
|
@@ -33,17 +35,25 @@ module Graphiti
|
|
33
35
|
@as = opts[:as]
|
34
36
|
@link = opts[:link]
|
35
37
|
@single = opts[:single]
|
38
|
+
@remote = opts[:remote]
|
36
39
|
apply_belongs_to_many_filter if type == :many_to_many
|
37
40
|
|
38
41
|
@description = opts[:description]
|
39
42
|
|
40
|
-
# polymorphic
|
43
|
+
# polymorphic has_many
|
44
|
+
@polymorphic_as = opts[:polymorphic_as]
|
45
|
+
# polymorphic_belongs_to-specific
|
41
46
|
@group_name = opts[:group_name]
|
42
47
|
@polymorphic_child = opts[:polymorphic_child]
|
43
48
|
@parent = opts[:parent]
|
44
49
|
if polymorphic_child?
|
45
50
|
parent.resource.polymorphic << resource_class
|
46
51
|
end
|
52
|
+
|
53
|
+
if remote?
|
54
|
+
@link = false
|
55
|
+
@resource_class = create_remote_resource
|
56
|
+
end
|
47
57
|
end
|
48
58
|
|
49
59
|
def self.scope(&blk)
|
@@ -70,10 +80,27 @@ module Graphiti
|
|
70
80
|
self.link_proc = blk
|
71
81
|
end
|
72
82
|
|
83
|
+
def create_remote_resource
|
84
|
+
_remote = @remote
|
85
|
+
klass = Class.new(Graphiti::Resource) do
|
86
|
+
self.adapter = Graphiti::Adapters::GraphitiAPI
|
87
|
+
self.model = OpenStruct
|
88
|
+
self.remote = _remote
|
89
|
+
self.validate_endpoints = false
|
90
|
+
end
|
91
|
+
name = "#{parent_resource_class.name}.#{@name}.remote"
|
92
|
+
klass.class_eval("def self.name;'#{name}';end")
|
93
|
+
klass
|
94
|
+
end
|
95
|
+
|
73
96
|
def errors
|
74
97
|
@errors ||= []
|
75
98
|
end
|
76
99
|
|
100
|
+
def remote?
|
101
|
+
!!@remote
|
102
|
+
end
|
103
|
+
|
77
104
|
def readable?
|
78
105
|
!!@readable
|
79
106
|
end
|
@@ -86,6 +113,10 @@ module Graphiti
|
|
86
113
|
!!@single
|
87
114
|
end
|
88
115
|
|
116
|
+
def polymorphic_has_many?
|
117
|
+
!!@polymorphic_as
|
118
|
+
end
|
119
|
+
|
89
120
|
def link?
|
90
121
|
return true if link_proc
|
91
122
|
|
@@ -96,6 +127,13 @@ module Graphiti
|
|
96
127
|
end
|
97
128
|
end
|
98
129
|
|
130
|
+
# The parent resource is a remote,
|
131
|
+
# AND the sideload is a remote to the same endpoint
|
132
|
+
def shared_remote?
|
133
|
+
resource.remote? &&
|
134
|
+
resource.remote_base_url = parent_resource_class.remote_base_url
|
135
|
+
end
|
136
|
+
|
99
137
|
def polymorphic_parent?
|
100
138
|
resource.polymorphic?
|
101
139
|
end
|
@@ -288,6 +326,18 @@ module Graphiti
|
|
288
326
|
|
289
327
|
private
|
290
328
|
|
329
|
+
def validate_options!(opts)
|
330
|
+
if opts[:remote]
|
331
|
+
if opts[:resource]
|
332
|
+
raise Errors::SideloadConfig.new(@name, opts[:parent_resource], 'cannot pass :remote and :resource options together')
|
333
|
+
end
|
334
|
+
|
335
|
+
if opts[:link]
|
336
|
+
raise Errors::SideloadConfig.new(@name, opts[:parent_resource], 'remote sideloads do not currently support :link')
|
337
|
+
end
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
291
341
|
def apply_belongs_to_many_filter
|
292
342
|
_self = self
|
293
343
|
fk_type = parent_resource_class.attributes[:id][:type]
|
@@ -48,6 +48,16 @@ class Graphiti::Sideload::BelongsTo < Graphiti::Sideload
|
|
48
48
|
end
|
49
49
|
|
50
50
|
def children_for(parent, map)
|
51
|
-
|
51
|
+
fk = parent.send(foreign_key)
|
52
|
+
children = map[fk]
|
53
|
+
return children if children
|
54
|
+
|
55
|
+
keys = map.keys
|
56
|
+
if fk.is_a?(String) && keys[0].is_a?(Integer)
|
57
|
+
fk = fk.to_i
|
58
|
+
elsif fk.is_a?(Integer) && keys[0].is_a?(String)
|
59
|
+
fk = fk.to_s
|
60
|
+
end
|
61
|
+
map[fk] || []
|
52
62
|
end
|
53
63
|
end
|
@@ -21,6 +21,16 @@ class Graphiti::Sideload::HasMany < Graphiti::Sideload
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def children_for(parent, map)
|
24
|
-
|
24
|
+
pk = parent.send(primary_key)
|
25
|
+
children = map[pk]
|
26
|
+
return children if children
|
27
|
+
|
28
|
+
keys = map.keys
|
29
|
+
if pk.is_a?(String) && keys[0].is_a?(Integer)
|
30
|
+
pk = pk.to_i
|
31
|
+
elsif pk.is_a?(Integer) && keys[0].is_a?(String)
|
32
|
+
pk = pk.to_s
|
33
|
+
end
|
34
|
+
map[pk] || []
|
25
35
|
end
|
26
36
|
end
|
@@ -83,6 +83,9 @@ class Graphiti::Util::Persistence
|
|
83
83
|
attrs[x[:foreign_key]] = nil
|
84
84
|
update_foreign_type(attrs, x, null: true) if x[:is_polymorphic]
|
85
85
|
else
|
86
|
+
if x[:sideload].polymorphic_has_many?
|
87
|
+
attrs[:"#{x[:sideload].polymorphic_as}_type"] = parent_object.class.name
|
88
|
+
end
|
86
89
|
attrs[x[:foreign_key]] = parent_object.send(x[:primary_key])
|
87
90
|
update_foreign_type(attrs, x) if x[:is_polymorphic]
|
88
91
|
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# Todo: class purpose
|
2
|
+
module Graphiti
|
3
|
+
module Util
|
4
|
+
class RemoteParams
|
5
|
+
def self.generate(resource, query)
|
6
|
+
new(resource, query).generate
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(resource, query)
|
10
|
+
@resource = resource
|
11
|
+
@query = query
|
12
|
+
@sorts = []
|
13
|
+
@filters = {}
|
14
|
+
@fields = {}
|
15
|
+
@extra_fields = {}
|
16
|
+
@pagination = {}
|
17
|
+
@params = {}
|
18
|
+
end
|
19
|
+
|
20
|
+
def generate
|
21
|
+
if include_hash = @query.include_hash.presence
|
22
|
+
@params[:include] = trim_sideloads(include_hash)
|
23
|
+
end
|
24
|
+
collect_params(@query)
|
25
|
+
@params[:sort] = @sorts.join(',') if @sorts.present?
|
26
|
+
@params[:filter] = @filters if @filters.present?
|
27
|
+
@params[:page] = @pagination if @pagination.present?
|
28
|
+
@params[:fields] = @fields if @fields.present?
|
29
|
+
@params[:extra_fields] = @extra_fields if @extra_fields.present?
|
30
|
+
@params[:stats] = @stats if @stats.present?
|
31
|
+
@params
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def collect_params(query)
|
37
|
+
query_hash = query.hash
|
38
|
+
process_sorts(query_hash[:sort], query)
|
39
|
+
process_fields(query_hash[:fields])
|
40
|
+
process_extra_fields(query_hash[:extra_fields])
|
41
|
+
process_filters(query_hash[:filter], query)
|
42
|
+
process_pagination(query_hash[:page], query)
|
43
|
+
process_stats(query_hash[:stats])
|
44
|
+
|
45
|
+
query.sideloads.each_pair do |assn_name, nested_query|
|
46
|
+
unless @resource.class.sideload(assn_name)
|
47
|
+
collect_params(nested_query)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def process_stats(stats)
|
53
|
+
return unless stats.present?
|
54
|
+
@stats = { stats.keys.first => stats.values.join(',') }
|
55
|
+
end
|
56
|
+
|
57
|
+
def process_pagination(page, query)
|
58
|
+
return unless page.present?
|
59
|
+
if size = page[:size]
|
60
|
+
key = (query.chain + [:size]).join('.')
|
61
|
+
@pagination[key.to_sym] = size
|
62
|
+
end
|
63
|
+
if number = page[:number]
|
64
|
+
key = (query.chain + [:number]).join('.')
|
65
|
+
@pagination[key.to_sym] = number
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def process_filters(filters, query)
|
70
|
+
return unless filters.present?
|
71
|
+
filters.each_pair do |att, config|
|
72
|
+
att = (query.chain + [att]).join('.')
|
73
|
+
@filters[att.to_sym] = config
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def process_fields(fields)
|
78
|
+
return unless fields
|
79
|
+
@fields[fields.keys.first.to_sym] = fields.values.join(',')
|
80
|
+
end
|
81
|
+
|
82
|
+
def process_extra_fields(fields)
|
83
|
+
return unless fields
|
84
|
+
@extra_fields[fields.keys.first.to_sym] = fields.values.join(',')
|
85
|
+
end
|
86
|
+
|
87
|
+
def process_sorts(sorts, query)
|
88
|
+
return unless sorts
|
89
|
+
|
90
|
+
if sorts.is_a?(String) # manually assigned
|
91
|
+
@sorts << sorts
|
92
|
+
else
|
93
|
+
sorts.each do |s|
|
94
|
+
sort = (query.chain + [s.keys.first]).join('.')
|
95
|
+
sort = "-#{sort}" if s.values.first == :desc
|
96
|
+
@sorts << sort
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Do not pass local sideloads to the remote endpoint
|
102
|
+
def trim_sideloads(include_hash)
|
103
|
+
return unless include_hash.present?
|
104
|
+
|
105
|
+
include_hash.each_pair do |assn_name, nested|
|
106
|
+
sideload = @resource.class.sideload(assn_name)
|
107
|
+
if sideload && !sideload.shared_remote?
|
108
|
+
include_hash.delete(assn_name)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
JSONAPI::IncludeDirective.new(include_hash).to_string
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Graphiti
|
2
|
+
module Util
|
3
|
+
class RemoteSerializer
|
4
|
+
def self.for(base, models)
|
5
|
+
new(base).generate(models)
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize(base)
|
9
|
+
@serializer = ::Class.new(base)
|
10
|
+
@serializer.type { @object._type }
|
11
|
+
end
|
12
|
+
|
13
|
+
def generate(models)
|
14
|
+
models.each do |model|
|
15
|
+
model.to_h.each_pair do |key, value|
|
16
|
+
if key == :_relationships
|
17
|
+
add_relationships(value)
|
18
|
+
else
|
19
|
+
@serializer.attribute(key) if add_attribute?(model, key)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
post_process(@serializer, models)
|
24
|
+
@serializer
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def add_relationships(relationship_hash)
|
30
|
+
relationship_hash.each_pair do |name, reldata|
|
31
|
+
@serializer.relationship(name.to_sym)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def add_attribute?(model, name)
|
36
|
+
disallow = [:_type, :id].include?(name)
|
37
|
+
pre_existing = @serializer.attribute_blocks[name]
|
38
|
+
is_relationship = model._relationships.try(:[], name.to_s)
|
39
|
+
!disallow && !pre_existing && !is_relationship
|
40
|
+
end
|
41
|
+
|
42
|
+
def post_process(serializer, models)
|
43
|
+
models.each do |model|
|
44
|
+
model.delete_field(:_relationships)
|
45
|
+
# If this isn't set, Array(resources) will return []
|
46
|
+
# This is important, because jsonapi-serializable makes this call
|
47
|
+
model.instance_variable_set(:@__graphiti_resource, 1)
|
48
|
+
model.instance_variable_set(:@__graphiti_serializer, serializer)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/lib/graphiti/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: graphiti
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.rc.
|
4
|
+
version: 1.0.rc.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lee Richmond
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-01-
|
11
|
+
date: 2019-01-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: jsonapi-serializable
|
@@ -86,6 +86,20 @@ dependencies:
|
|
86
86
|
- - "<"
|
87
87
|
- !ruby/object:Gem::Version
|
88
88
|
version: '6'
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: faraday
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0.15'
|
96
|
+
type: :development
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - "~>"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0.15'
|
89
103
|
- !ruby/object:Gem::Dependency
|
90
104
|
name: activerecord
|
91
105
|
requirement: !ruby/object:Gem::Requirement
|
@@ -260,6 +274,7 @@ files:
|
|
260
274
|
- lib/graphiti/adapters/active_record/has_one_sideload.rb
|
261
275
|
- lib/graphiti/adapters/active_record/inferrence.rb
|
262
276
|
- lib/graphiti/adapters/active_record/many_to_many_sideload.rb
|
277
|
+
- lib/graphiti/adapters/graphiti_api.rb
|
263
278
|
- lib/graphiti/adapters/null.rb
|
264
279
|
- lib/graphiti/base.rb
|
265
280
|
- lib/graphiti/cli.rb
|
@@ -286,6 +301,7 @@ files:
|
|
286
301
|
- lib/graphiti/resource/links.rb
|
287
302
|
- lib/graphiti/resource/persistence.rb
|
288
303
|
- lib/graphiti/resource/polymorphism.rb
|
304
|
+
- lib/graphiti/resource/remote.rb
|
289
305
|
- lib/graphiti/resource/sideloading.rb
|
290
306
|
- lib/graphiti/resource_proxy.rb
|
291
307
|
- lib/graphiti/responders.rb
|
@@ -320,6 +336,8 @@ files:
|
|
320
336
|
- lib/graphiti/util/link.rb
|
321
337
|
- lib/graphiti/util/persistence.rb
|
322
338
|
- lib/graphiti/util/relationship_payload.rb
|
339
|
+
- lib/graphiti/util/remote_params.rb
|
340
|
+
- lib/graphiti/util/remote_serializer.rb
|
323
341
|
- lib/graphiti/util/serializer_attributes.rb
|
324
342
|
- lib/graphiti/util/serializer_relationships.rb
|
325
343
|
- lib/graphiti/util/sideload.rb
|