graphiti 1.0.rc.2 → 1.0.rc.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 26fc322fc29632b3e24f85bcbe535a3542c0d142
4
- data.tar.gz: a7125ea4aada6e5d04d4f58109ef931593f39273
3
+ metadata.gz: 673ff2ee2d99cc9cb04d8cfb86b5b2b03839b227
4
+ data.tar.gz: a07ef309b47401d52a75f9bdbd6675d09fd83117
5
5
  SHA512:
6
- metadata.gz: 7e7410a027fa7a72a15fb90af367680069c9c1842a83aa96eb137e77596880bd803fa8478379c7724213930cc1c51db150bacd18e62681d9cb0d6080a16c0227
7
- data.tar.gz: 01a535771b79dcaa05a78cf7a938e3a59261380762c6dd8118b889339cff0647774d8d8ca881c55ddcea361f18dc3857089bea67d0800a5cc4fe4b783f76f482
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
@@ -17,7 +17,9 @@ module Graphiti
17
17
  on_data_exception(payload, params)
18
18
  else
19
19
  if payload[:sideload]
20
- on_sideload_data(payload, params, took)
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
@@ -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
@@ -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
- if sideload
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(sideload.resource, @params, key, sub_hash, _parents)
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.get_attr!(key, :sortable, request: true)
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
@@ -106,6 +106,10 @@ module Graphiti
106
106
  stats_dsl.calculation(calculation)
107
107
  end
108
108
 
109
+ def before_resolve(scope, query)
110
+ scope
111
+ end
112
+
109
113
  def resolve(scope)
110
114
  adapter.resolve(scope)
111
115
  end
@@ -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
@@ -63,6 +63,7 @@ module Graphiti
63
63
 
64
64
  def polymorphic_has_many(name, opts = {}, as:, &blk)
65
65
  opts[:foreign_key] ||= :"#{as}_id"
66
+ opts[:polymorphic_as] ||= as
66
67
  _model = model
67
68
  has_many name, opts do
68
69
  params do |hash|
@@ -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] = action
87
- validator = persist do
88
- @resource.persist_with_relationships \
89
- @payload.meta(action: action),
90
- @payload.attributes,
91
- @payload.relationships
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
 
@@ -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)
@@ -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
- opts[:default_paginate] = false unless @query.paginate?
98
- add_scoping(nil, Graphiti::Scoping::DefaultFilter, opts)
99
- add_scoping(:filter, Graphiti::Scoping::Filter, opts)
100
- add_scoping(:sort, Graphiti::Scoping::Sort, opts)
101
- add_scoping(:paginate, Graphiti::Scoping::Paginate, opts)
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
 
@@ -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-specific
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
- map[parent.send(foreign_key)]
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
- map[parent.send(primary_key)] || []
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
@@ -1,3 +1,3 @@
1
1
  module Graphiti
2
- VERSION = "1.0.rc.2"
2
+ VERSION = "1.0.rc.3"
3
3
  end
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.2
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-08 00:00:00.000000000 Z
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