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 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