graphiti 1.2.27 → 1.2.32

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
  SHA256:
3
- metadata.gz: fb8f1e8668da2d5d53c211b2f999a5109bf968815f1a700be6d5ee29537afc21
4
- data.tar.gz: c60dd4731e33f6b495b82575989dc72376697ec21bbcbb01ce23021c61dbc26b
3
+ metadata.gz: 9ec84a0ed1957831fa1ae6bec24a6470d5bb16189681e096b5d59a468b95b49a
4
+ data.tar.gz: 145633f22cad8f867ce295337a0b3bc5adfc45da6695b481abb092d0f7b17ed8
5
5
  SHA512:
6
- metadata.gz: ecd5b910c32aff8592b16406fb8cf12e3d8d00585e671900966c594e475ea9a4d7b620d7d7ff52d35aad9190563fc29f5ce0e1014d886ee35ed46b72e1791ca8
7
- data.tar.gz: e1273d26d0571ae89f0405933888ac17b10083cfe250da9cb7d7118a511efe381cc03176ec9f78e30e5157d2ec2588883c6d113a22a84a0d326e9ccf02b016dd
6
+ metadata.gz: a841e2eceebba1e495608ec2037a1c7deffa408fc7e408756a6edbfed0cfaded42d82abf55899724ec201a427a6f8226bbe554142739ff1067133d9d499835bd
7
+ data.tar.gz: 3e6b741c6942c8b5ae89b08c7d5fdc6eb71b4c1b82794788207813bc914f0f245c43f196d603e2478c7dba61145403c507e1d7937028f8e217e71144beb2c128
data/.travis.yml CHANGED
@@ -19,7 +19,13 @@ env:
19
19
  - COMMAND="standardrb --no-fix --format progress"
20
20
  - COMMAND=rspec
21
21
  - COMMAND=rspec APPRAISAL_INITIALIZED=true
22
- matrix:
22
+ jobs:
23
+ allow_failures:
24
+ - rvm: ruby-head
25
+ include:
26
+ - env: COMMAND=rspec
27
+ gemfile: Gemfile
28
+ rvm: ruby-head
23
29
  exclude:
24
30
  # Don't run the appraisal version of the specs for the base gemfile
25
31
  - env: COMMAND=rspec APPRAISAL_INITIALIZED=true
data/CHANGELOG.md CHANGED
@@ -8,6 +8,10 @@ Features:
8
8
  - [157](https://github.com/graphiti-api/graphiti/pull/157) Using attribute option schema: false.
9
9
  This option is default true and is not effected by only and except options. (@zeisler)
10
10
 
11
+ Fixes:
12
+ - [282] Support model names including "Resource"
13
+ - [313](https://github.com/graphiti-api/graphiti/pull/313) Sort remote resources in schema generation
14
+
11
15
  ## 1.1.0
12
16
 
13
17
  Features:
@@ -12,6 +12,7 @@ module Graphiti
12
12
  def api_namespace
13
13
  @api_namespace ||= begin
14
14
  ns = graphiti_config["namespace"]
15
+ ns.delete_suffix("/")
15
16
 
16
17
  if ns.blank?
17
18
  ns = prompt \
data/graphiti.gemspec CHANGED
@@ -16,7 +16,7 @@ Gem::Specification.new do |spec|
16
16
  spec.bindir = "exe"
17
17
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
18
18
  spec.require_paths = ["lib"]
19
- spec.required_ruby_version = "~> 2.3"
19
+ spec.required_ruby_version = [">= 2.3", "< 3.1"]
20
20
 
21
21
  spec.add_dependency "jsonapi-serializable", "~> 0.3.0"
22
22
  spec.add_dependency "jsonapi-renderer", "~> 0.2", ">= 0.2.2"
@@ -1,6 +1,9 @@
1
1
  module Graphiti
2
2
  module Adapters
3
3
  class Abstract
4
+ require "graphiti/adapters/persistence/associations.rb"
5
+ include Graphiti::Adapters::Persistence::Associations
6
+
4
7
  attr_reader :resource
5
8
 
6
9
  def initialize(resource)
@@ -400,6 +403,13 @@ module Graphiti
400
403
  raise "you must override #destroy in an adapter subclass"
401
404
  end
402
405
 
406
+ def close
407
+ end
408
+
409
+ def persistence_attributes(persistance, attributes)
410
+ attributes
411
+ end
412
+
403
413
  def self.numerical_operators
404
414
  [:eq, :not_eq, :gt, :gte, :lt, :lte].freeze
405
415
  end
@@ -191,7 +191,7 @@ module Graphiti
191
191
  # (see Adapters::Abstract#count)
192
192
  def count(scope, attr)
193
193
  if attr.to_sym == :total
194
- scope.distinct.count
194
+ scope.distinct.count(:all)
195
195
  else
196
196
  scope.distinct.count(attr)
197
197
  end
@@ -297,6 +297,10 @@ module Graphiti
297
297
  model_instance
298
298
  end
299
299
 
300
+ def close
301
+ ::ActiveRecord::Base.clear_active_connections!
302
+ end
303
+
300
304
  private
301
305
 
302
306
  def column_for(scope, name)
@@ -34,7 +34,7 @@ module Graphiti
34
34
 
35
35
  def build_url(scope)
36
36
  url = resource.remote_url
37
- params = scope[:params].merge(scope.except(:params))
37
+ params = scope[:params].merge(scope.except(:params, :foreign_key))
38
38
  params[:page] ||= {}
39
39
  params[:page][:size] ||= 999
40
40
  params = CGI.unescape(params.to_query)
@@ -0,0 +1,80 @@
1
+ module Graphiti
2
+ module Adapters
3
+ module Persistence
4
+ module Associations
5
+ def process_belongs_to(persistence, attributes)
6
+ parents = [].tap do |processed|
7
+ persistence.iterate(only: [:polymorphic_belongs_to, :belongs_to]) do |x|
8
+ begin
9
+ id = x.dig(:attributes, :id)
10
+ x[:object] = x[:resource]
11
+ .persist_with_relationships(x[:meta], x[:attributes], x[:relationships])
12
+ processed << x
13
+ rescue Graphiti::Errors::RecordNotFound
14
+ if Graphiti.config.raise_on_missing_sidepost
15
+ path = "relationships/#{x.dig(:meta, :jsonapi_type)}"
16
+ raise Graphiti::Errors::RecordNotFound.new(x[:sideload].name, id, path)
17
+ else
18
+ pointer = "data/relationships/#{x.dig(:meta, :jsonapi_type)}"
19
+ object = Graphiti::Errors::NullRelation.new(id.to_s, pointer)
20
+ object.errors.add(:base, :not_found, message: "could not be found")
21
+ x[:object] = object
22
+ processed << x
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ update_foreign_key_for_parents(parents, attributes)
29
+ parents
30
+ end
31
+
32
+ def process_has_many(persistence, caller_model)
33
+ [].tap do |processed|
34
+ persistence.iterate(except: [:polymorphic_belongs_to, :belongs_to]) do |x|
35
+ update_foreign_key(caller_model, x[:attributes], x)
36
+
37
+ x[:object] = x[:resource]
38
+ .persist_with_relationships(x[:meta], x[:attributes], x[:relationships], caller_model, x[:foreign_key])
39
+
40
+ processed << x
41
+ end
42
+ end
43
+ end
44
+
45
+ def update_foreign_key_for_parents(parents, attributes)
46
+ parents.each do |x|
47
+ update_foreign_key(x[:object], attributes, x)
48
+ end
49
+ end
50
+
51
+ # The child's attributes should be modified to nil-out the
52
+ # foreign_key when the parent is being destroyed or disassociated
53
+ #
54
+ # This is not the case for HABTM, whose "foreign key" is a join table
55
+ def update_foreign_key(parent_object, attrs, x)
56
+ return if x[:sideload].type == :many_to_many
57
+
58
+ if [:destroy, :disassociate].include?(x[:meta][:method])
59
+ if x[:sideload].polymorphic_has_one? || x[:sideload].polymorphic_has_many?
60
+ attrs[:"#{x[:sideload].polymorphic_as}_type"] = nil
61
+ end
62
+ attrs[x[:foreign_key]] = nil
63
+ update_foreign_type(attrs, x, null: true) if x[:is_polymorphic]
64
+ else
65
+ if x[:sideload].polymorphic_has_one? || x[:sideload].polymorphic_has_many?
66
+ attrs[:"#{x[:sideload].polymorphic_as}_type"] = parent_object.class.name
67
+ end
68
+ attrs[x[:foreign_key]] = parent_object.send(x[:primary_key])
69
+ update_foreign_type(attrs, x) if x[:is_polymorphic]
70
+ end
71
+ end
72
+
73
+ def update_foreign_type(attrs, x, null: false)
74
+ grouping_field = x[:sideload].parent.grouper.field_name
75
+ attrs[grouping_field] = null ? nil : x[:sideload].group_name
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -14,6 +14,7 @@ module Graphiti
14
14
  attr_accessor :pagination_links_on_demand
15
15
  attr_accessor :pagination_links
16
16
  attr_accessor :typecast_reads
17
+ attr_accessor :raise_on_missing_sidepost
17
18
 
18
19
  attr_reader :debug, :debug_models
19
20
 
@@ -29,6 +30,7 @@ module Graphiti
29
30
  @pagination_links_on_demand = false
30
31
  @pagination_links = false
31
32
  @typecast_reads = true
33
+ @raise_on_missing_sidepost = true
32
34
  self.debug = ENV.fetch("GRAPHITI_DEBUG", true)
33
35
  self.debug_models = ENV.fetch("GRAPHITI_DEBUG_MODELS", false)
34
36
 
@@ -43,7 +45,7 @@ module Graphiti
43
45
  end
44
46
 
45
47
  if (logger = ::Rails.logger)
46
- self.debug = logger.level.zero? && debug
48
+ self.debug = logger.debug? && debug
47
49
  Graphiti.logger = logger
48
50
  end
49
51
  end
@@ -6,11 +6,12 @@ module Graphiti
6
6
  end
7
7
 
8
8
  def links?
9
- @proxy.query.pagination_links?
9
+ @proxy.query.pagination_links? && @proxy.data.present?
10
10
  end
11
11
 
12
12
  def links
13
13
  @links ||= {}.tap do |links|
14
+ links[:self] = pagination_link(current_page)
14
15
  links[:first] = pagination_link(1)
15
16
  links[:last] = pagination_link(last_page)
16
17
  links[:prev] = pagination_link(current_page - 1) unless current_page == 1
@@ -53,7 +54,7 @@ module Graphiti
53
54
  def item_count
54
55
  begin
55
56
  return @item_count if @item_count
56
- @item_count = @proxy.resource.stat(:total, :count).call(@proxy.scope.unpaginated_object, :total)
57
+ @item_count = item_count_from_proxy || item_count_from_stats
57
58
  unless @item_count.is_a?(Numeric)
58
59
  raise TypeError, "#{@proxy.resource}.stat(:total, :count) returned an invalid value #{@item_count}"
59
60
  end
@@ -68,6 +69,15 @@ module Graphiti
68
69
  @item_count
69
70
  end
70
71
 
72
+ def item_count_from_proxy
73
+ @proxy.stats.dig(:total, :count)
74
+ end
75
+
76
+ def item_count_from_stats
77
+ stats = Stats::Payload.new(@proxy.resource, @proxy.query, @proxy.scope.unpaginated_object, @proxy.data)
78
+ stats.calculate_stat(:total, @proxy.resource.stat(:total, :count))
79
+ end
80
+
71
81
  def current_page
72
82
  @current_page ||= (page_param[:number] || 1).to_i
73
83
  end
@@ -2,6 +2,32 @@ module Graphiti
2
2
  module Errors
3
3
  class Base < StandardError; end
4
4
 
5
+ class UnreadableAttribute < Base
6
+ def initialize(resource_class, name)
7
+ @resource_class = resource_class
8
+ @name = name
9
+ end
10
+
11
+ def message
12
+ "#{@resource_class}: Requested field #{@name}, but not authorized to read it"
13
+ end
14
+ end
15
+
16
+ class NullRelation
17
+ extend ActiveModel::Naming
18
+ attr_accessor :id, :errors, :pointer
19
+
20
+ def initialize(id, pointer)
21
+ @id = id
22
+ @pointer = pointer
23
+ @errors = ActiveModel::Errors.new(self)
24
+ end
25
+
26
+ def self.human_attribute_name(attr, options = {})
27
+ attr
28
+ end
29
+ end
30
+
5
31
  class AdapterNotImplemented < Base
6
32
  def initialize(adapter, attribute, method)
7
33
  @adapter = adapter
@@ -1,39 +1,138 @@
1
1
  module Graphiti
2
2
  module SerializableHash
3
- def to_hash(fields: nil, include: {})
3
+ def to_hash(fields: nil, include: {}, name_chain: [], graphql: false)
4
4
  {}.tap do |hash|
5
- fields_list = fields[jsonapi_type] if fields
5
+ if fields
6
+ fields_list = nil
7
+
8
+ # Dot syntax wins over jsonapi type
9
+ if name_chain.length > 0
10
+ fields_list = fields[name_chain.join(".").to_sym]
11
+ end
12
+
13
+ if fields_list.nil?
14
+ fields_list = fields[jsonapi_type]
15
+ end
16
+ end
17
+
18
+ # polymorphic resources - merge the PARENT type
19
+ if polymorphic_subclass?
20
+ if fields[@resource.type]
21
+ fields_list ||= []
22
+ fields_list |= fields[@resource.type]
23
+ end
24
+
25
+ if fields[jsonapi_type]
26
+ fields_list ||= []
27
+ fields_list |= fields[jsonapi_type]
28
+ end
29
+ end
30
+
6
31
  attrs = requested_attributes(fields_list).each_with_object({}) { |(k, v), h|
7
- h[k] = instance_eval(&v)
32
+ name = graphql ? k.to_s.camelize(:lower).to_sym : k
33
+ h[name] = instance_eval(&v)
8
34
  }
9
- rels = @_relationships.select { |k, v| !!include[k] }
35
+
36
+ # The main logic here is just !!include[k]
37
+ # But we also have special on__<type>--<name> includes
38
+ # Where we only include when matching the polymorphic type
39
+ rels = @_relationships.select { |k, v|
40
+ if include[k]
41
+ true
42
+ else
43
+ included = false
44
+ include.keys.each do |key|
45
+ split = key.to_s.split(/^on__/)
46
+ if split.length > 1
47
+ requested_type, key = split[1].split("--")
48
+ if requested_type.to_sym == jsonapi_type
49
+ included = k == key.to_sym
50
+ break
51
+ end
52
+ end
53
+ end
54
+ included
55
+ end
56
+ }
57
+
10
58
  rels.each_with_object({}) do |(k, v), h|
59
+ nested_include = include[k]
60
+
61
+ # This logic only fires if it's a special on__<type>--<name> include
62
+ unless include.has_key?(k)
63
+ include.keys.each do |include_key|
64
+ if k == include_key.to_s.split("--")[1].to_sym
65
+ nested_include = include[include_key]
66
+ break
67
+ end
68
+ end
69
+ end
70
+
11
71
  serializers = v.send(:resources)
12
- attrs[k] = if serializers.is_a?(Array)
13
- serializers.map do |rr| # use private method to avoid array casting
14
- rr.to_hash(fields: fields, include: include[k])
72
+ name = graphql ? k.to_s.camelize(:lower) : k
73
+ name_chain = name_chain.dup
74
+ name_chain << k unless name_chain.last == k
75
+
76
+ unless remote_resource? && serializers.nil?
77
+ attrs[name.to_sym] = if serializers.is_a?(Array)
78
+ serializers.map do |rr|
79
+ rr.to_hash(fields: fields, include: nested_include, graphql: graphql, name_chain: name_chain)
80
+ end
81
+ elsif serializers.nil?
82
+ if @resource.class.respond_to?(:sideload)
83
+ if @resource.class.sideload(k).type.to_s.include?("_many")
84
+ []
85
+ end
86
+ end
87
+ else
88
+ serializers.to_hash(fields: fields, include: nested_include, graphql: graphql, name_chain: name_chain)
15
89
  end
16
- elsif serializers.nil?
17
- nil
18
- else
19
- serializers.to_hash(fields: fields, include: include[k])
20
90
  end
21
91
  end
22
92
 
23
- hash[:id] = jsonapi_id
93
+ if !graphql || (fields_list || []).include?(:id)
94
+ hash[:id] = jsonapi_id
95
+ end
96
+
97
+ if (fields_list || []).include?(:_type)
98
+ hash[:_type] = jsonapi_type.to_s
99
+ end
100
+
101
+ if (fields_list || []).include?(:__typename)
102
+ resource_class = @resource.class
103
+ if polymorphic_subclass?
104
+ resource_class = @resource.class.resource_for_type(jsonapi_type)
105
+ end
106
+ hash[:__typename] = ::Graphiti::Util::Class
107
+ .graphql_type_name(resource_class.name)
108
+ end
109
+
24
110
  hash.merge!(attrs) if attrs.any?
25
111
  end
26
112
  end
113
+
114
+ def polymorphic_subclass?
115
+ !remote_resource? &&
116
+ @resource.polymorphic? &&
117
+ @resource.type != jsonapi_type
118
+ end
119
+
120
+ # See hack in util/remote_serializer.rb
121
+ def remote_resource?
122
+ @resource == 1
123
+ end
27
124
  end
28
125
 
29
126
  class HashRenderer
30
- def initialize(resource)
127
+ def initialize(resource, graphql: false)
31
128
  @resource = resource
129
+ @graphql = graphql
32
130
  end
33
131
 
34
132
  def render(options)
35
133
  serializers = options[:data]
36
134
  opts = options.slice(:fields, :include)
135
+ opts[:graphql] = @graphql
37
136
  to_hash(serializers, opts).tap do |hash|
38
137
  hash.merge!(options.slice(:meta)) unless options[:meta].empty?
39
138
  end
@@ -43,12 +142,20 @@ module Graphiti
43
142
 
44
143
  def to_hash(serializers, opts)
45
144
  {}.tap do |hash|
46
- hash[:data] = if serializers.is_a?(Array)
145
+ top_level_key = :data
146
+ if @graphql
147
+ top_level_key = @resource.graphql_entrypoint
148
+ unless serializers.is_a?(Array)
149
+ top_level_key = top_level_key.to_s.singularize.to_sym
150
+ end
151
+ end
152
+
153
+ hash[top_level_key] = if serializers.is_a?(Array)
47
154
  serializers.map do |s|
48
- s.to_hash(opts)
155
+ s.to_hash(**opts)
49
156
  end
50
157
  else
51
- serializers.to_hash(opts)
158
+ serializers.to_hash(**opts)
52
159
  end
53
160
  end
54
161
  end
@@ -96,7 +96,18 @@ module Graphiti
96
96
  sl_resource = resource_for_sideload(sideload)
97
97
  query_parents = parents + [self]
98
98
  sub_hash = sub_hash[:include] if sub_hash.key?(:include)
99
- hash[key] = Query.new(sl_resource, @params, key, sub_hash, query_parents, :all)
99
+
100
+ # NB: To handle on__<type>--<name>
101
+ # A) relationship_name == :positions
102
+ # B) key == on__employees.positions
103
+ # This way A) ensures sideloads are resolved
104
+ # And B) ensures nested filters, sorts etc still work
105
+ relationship_name = sideload ? sideload.name : key
106
+ hash[relationship_name] = Query.new sl_resource,
107
+ @params,
108
+ key,
109
+ sub_hash,
110
+ query_parents, :all
100
111
  else
101
112
  handle_missing_sideload(key)
102
113
  end
@@ -17,8 +17,20 @@ module Graphiti
17
17
  render(self.class.jsonapi_renderer).to_json
18
18
  end
19
19
 
20
+ def as_graphql
21
+ render(self.class.graphql_renderer(@proxy))
22
+ end
23
+
24
+ def to_graphql
25
+ as_graphql.to_json
26
+ end
27
+
20
28
  def to_json
21
- render(self.class.hash_renderer(@proxy)).to_json
29
+ as_json.to_json
30
+ end
31
+
32
+ def as_json
33
+ render(self.class.hash_renderer(@proxy))
22
34
  end
23
35
 
24
36
  def to_xml
@@ -35,6 +47,11 @@ module Graphiti
35
47
  JSONAPI::Serializable::Renderer.new(implementation)
36
48
  end
37
49
 
50
+ def self.graphql_renderer(proxy)
51
+ implementation = Graphiti::HashRenderer.new(proxy.resource, graphql: true)
52
+ JSONAPI::Serializable::Renderer.new(implementation)
53
+ end
54
+
38
55
  private
39
56
 
40
57
  def render(renderer)
@@ -51,7 +51,7 @@ module Graphiti
51
51
  relationships: relationships
52
52
  }
53
53
 
54
- Graphiti::Util::RelationshipPayload.iterate(opts) do |x|
54
+ Graphiti::Util::RelationshipPayload.iterate(**opts) do |x|
55
55
  sideload_def = x[:sideload]
56
56
 
57
57
  unless sideload_def.writable?
@@ -83,7 +83,8 @@ module Graphiti
83
83
  :relationships_readable_by_default,
84
84
  :relationships_writable_by_default,
85
85
  :filters_accept_nil_by_default,
86
- :filters_deny_empty_by_default
86
+ :filters_deny_empty_by_default,
87
+ :graphql_entrypoint
87
88
 
88
89
  class << self
89
90
  prepend Overrides
@@ -97,6 +98,7 @@ module Graphiti
97
98
  # re-assigning causes a new Class.new
98
99
  klass.serializer = (klass.serializer || klass.infer_serializer_superclass)
99
100
  klass.type ||= klass.infer_type
101
+ klass.graphql_entrypoint = klass.type.to_s.pluralize.to_sym
100
102
  default(klass, :attributes_readable_by_default, true)
101
103
  default(klass, :attributes_writable_by_default, true)
102
104
  default(klass, :attributes_sortable_by_default, true)
@@ -129,7 +131,7 @@ module Graphiti
129
131
  def get_attr(name, flag, opts = {})
130
132
  defaults = {request: false}
131
133
  opts = defaults.merge(opts)
132
- new.get_attr(name, flag, opts)
134
+ new.get_attr(name, flag, **opts)
133
135
  end
134
136
 
135
137
  def abstract_class?
@@ -144,19 +146,20 @@ module Graphiti
144
146
  if (@abstract_class = val)
145
147
  self.serializer = nil
146
148
  self.type = nil
149
+ self.graphql_entrypoint = nil
147
150
  end
148
151
  end
149
152
 
150
153
  def infer_type
151
154
  if name.present?
152
- name.demodulize.gsub("Resource", "").underscore.pluralize.to_sym
155
+ name.demodulize.sub(/.*\KResource/, "").underscore.pluralize.to_sym
153
156
  else
154
157
  :undefined_jsonapi_type
155
158
  end
156
159
  end
157
160
 
158
161
  def infer_model
159
- name&.gsub("Resource", "")&.safe_constantize
162
+ name&.sub(/.*\KResource/, "")&.safe_constantize
160
163
  end
161
164
 
162
165
  # @api private
@@ -247,7 +250,7 @@ module Graphiti
247
250
 
248
251
  def get_attr!(name, flag, options = {})
249
252
  options[:raise_error] = true
250
- get_attr(name, flag, options)
253
+ get_attr(name, flag, **options)
251
254
  end
252
255
 
253
256
  def get_attr(name, flag, request: false, raise_error: false)
@@ -46,7 +46,7 @@ module Graphiti
46
46
  private
47
47
 
48
48
  def validate!(params)
49
- return unless validate_endpoints?
49
+ return if Graphiti.context[:graphql] || !validate_endpoints?
50
50
 
51
51
  if context&.respond_to?(:request)
52
52
  path = context.request.env["PATH_INFO"]
@@ -42,9 +42,14 @@ module Graphiti
42
42
  end
43
43
 
44
44
  def sideload(name)
45
- sl = super
45
+ if (split_on = name.to_s.split(/^on__/)).length > 1
46
+ on_type, name = split_on[1].split("--").map(&:to_sym)
47
+ end
48
+
49
+ sl = super(name)
46
50
  if !polymorphic_child? && sl.nil?
47
51
  children.each do |c|
52
+ next if on_type && c.type != on_type
48
53
  break if (sl = c.sideloads[name])
49
54
  end
50
55
  end
@@ -31,7 +31,7 @@ module Graphiti
31
31
  end
32
32
 
33
33
  def before_resolve(scope, query)
34
- scope[:params] = Util::RemoteParams.generate(self, query)
34
+ scope[:params] = Util::RemoteParams.generate(self, query, scope[:foreign_key])
35
35
  scope
36
36
  end
37
37
 
@@ -47,10 +47,22 @@ module Graphiti
47
47
  Renderer.new(self, options).to_json
48
48
  end
49
49
 
50
+ def as_json(options = {})
51
+ Renderer.new(self, options).as_json
52
+ end
53
+
50
54
  def to_xml(options = {})
51
55
  Renderer.new(self, options).to_xml
52
56
  end
53
57
 
58
+ def to_graphql(options = {})
59
+ Renderer.new(self, options).to_graphql
60
+ end
61
+
62
+ def as_graphql(options = {})
63
+ Renderer.new(self, options).as_graphql
64
+ end
65
+
54
66
  def data
55
67
  @data ||= begin
56
68
  records = @scope.resolve
@@ -7,6 +7,7 @@ module Graphiti
7
7
  ::Rails.application.eager_load! if defined?(::Rails)
8
8
  resources ||= Graphiti.resources.reject(&:abstract_class?)
9
9
  resources.reject! { |r| r.name.nil? }
10
+
10
11
  new(resources).generate
11
12
  end
12
13
 
@@ -25,7 +26,7 @@ module Graphiti
25
26
 
26
27
  def initialize(resources)
27
28
  @resources = resources.sort_by(&:name)
28
- @remote_resources = resources.select(&:remote?)
29
+ @remote_resources = @resources.select(&:remote?)
29
30
  @local_resources = @resources - @remote_resources
30
31
  end
31
32
 
@@ -89,6 +90,7 @@ module Graphiti
89
90
  config = {
90
91
  name: r.name,
91
92
  type: r.type.to_s,
93
+ graphql_entrypoint: r.graphql_entrypoint.to_s,
92
94
  description: r.description,
93
95
  attributes: attributes(r),
94
96
  extra_attributes: extra_attributes(r),
@@ -108,7 +110,7 @@ module Graphiti
108
110
  config[:default_page_size] = r.default_page_size
109
111
  end
110
112
 
111
- if r.polymorphic?
113
+ if r.polymorphic? && !r.polymorphic_child?
112
114
  config[:polymorphic] = true
113
115
  config[:children] = r.children.map(&:name)
114
116
  end
@@ -202,6 +204,9 @@ module Graphiti
202
204
  config[:guard] = true
203
205
  end
204
206
  end
207
+ if filter[:required] # one-off filter, not attribute
208
+ config[:required] = true
209
+ end
205
210
  f[name] = config
206
211
  end
207
212
  end
@@ -214,6 +219,7 @@ module Graphiti
214
219
  if config.type == :polymorphic_belongs_to
215
220
  schema[:resources] = config.children.values
216
221
  .map(&:resource).map(&:class).map(&:name)
222
+ schema[:parent_resource] = config.parent_resource.class.name
217
223
  else
218
224
  schema[:resource] = config.resource.class.name
219
225
  end
@@ -45,9 +45,7 @@ module Graphiti
45
45
  resolve_sideload = -> {
46
46
  Graphiti.context = graphiti_context
47
47
  sideload.resolve(results, q, parent_resource)
48
- if concurrent && defined?(ActiveRecord)
49
- ActiveRecord::Base.clear_active_connections!
50
- end
48
+ @resource.adapter.close if concurrent
51
49
  }
52
50
  if concurrent
53
51
  promises << Concurrent::Promise.execute(&resolve_sideload)
@@ -12,29 +12,46 @@ module Graphiti
12
12
  # go through type checking/coercion
13
13
  class_attribute :attributes_applied_via_resource
14
14
  class_attribute :extra_attributes_applied_via_resource
15
+ class_attribute :relationship_condition_blocks
15
16
  self.attributes_applied_via_resource = []
16
17
  self.extra_attributes_applied_via_resource = []
18
+ # See #requested_relationships
19
+ self.relationship_condition_blocks ||= {}
17
20
 
18
21
  def self.inherited(klass)
19
22
  super
20
23
  klass.class_eval do
21
24
  extend JSONAPI::Serializable::Resource::ConditionalFields
25
+
26
+ # See #requested_relationships
27
+ def self.relationship(name, options = {}, &block)
28
+ super
29
+ field_condition_blocks.delete(name)
30
+ _register_condition(relationship_condition_blocks, name, options)
31
+ end
32
+
33
+ # NB - avoid clobbering includes when sparse fieldset
34
+ # https://github.com/jsonapi-rb/jsonapi-serializable/pull/102
35
+ #
36
+ # We also override this method to ensure attributes and relationships
37
+ # have separate condition blocks. This way an attribute and
38
+ # relationship can have the same name, and the attribute can be
39
+ # conditional without affecting the relationship.
40
+ def requested_relationships(fields)
41
+ @_relationships.select do |k, _|
42
+ _conditionally_included?(self.class.relationship_condition_blocks, k)
43
+ end
44
+ end
22
45
  end
23
46
  end
24
47
 
25
- def as_jsonapi(*)
26
- super.tap do |hash|
48
+ def as_jsonapi(kwargs = {})
49
+ super(**kwargs).tap do |hash|
27
50
  strip_relationships!(hash) if strip_relationships?
28
51
  add_links!(hash)
29
52
  end
30
53
  end
31
54
 
32
- # Temporary fix until fixed upstream
33
- # https://github.com/jsonapi-rb/jsonapi-serializable/pull/102
34
- def requested_relationships(fields)
35
- @_relationships
36
- end
37
-
38
55
  # Allow access to resource methods
39
56
  def method_missing(id, *args, &blk)
40
57
  if @resource.respond_to?(id, true)
@@ -26,8 +26,8 @@ module Graphiti
26
26
  @foreign_key = opts[:foreign_key]
27
27
  @type = opts[:type]
28
28
  @base_scope = opts[:base_scope]
29
- @readable = evaluate_flag(opts[:readable])
30
- @writable = evaluate_flag(opts[:writable])
29
+ @readable = opts[:readable]
30
+ @writable = opts[:writable]
31
31
  @as = opts[:as]
32
32
  @link = opts[:link]
33
33
  @single = opts[:single]
@@ -209,7 +209,9 @@ module Graphiti
209
209
  end
210
210
 
211
211
  with_error_handling(Errors::SideloadQueryBuildingError) do
212
- proxy = resource.class._all(params, opts, base_scope)
212
+ scope = base_scope
213
+ scope[:foreign_key] = foreign_key if remote?
214
+ proxy = resource.class._all(params, opts, scope)
213
215
  pre_load_proc&.call(proxy, parents)
214
216
  end
215
217
 
@@ -426,6 +428,7 @@ module Graphiti
426
428
  Util::Class.namespace_for(klass)
427
429
  end
428
430
 
431
+ # TODO: call this at runtime to support procs
429
432
  def evaluate_flag(flag)
430
433
  return false if flag.blank?
431
434
 
@@ -36,9 +36,12 @@ class Graphiti::Sideload::ManyToMany < Graphiti::Sideload::HasMany
36
36
  self_ref = self
37
37
  fk_type = parent_resource_class.attributes[:id][:type]
38
38
  fk_type = :hash if polymorphic?
39
- resource_class.filter inverse_filter, fk_type do
40
- eq do |scope, value|
41
- self_ref.belongs_to_many_filter(scope, value)
39
+ # Do not recreate if filter already exists
40
+ unless resource_class.config[:filters].has_key?(inverse_filter.to_sym)
41
+ resource_class.filter inverse_filter, fk_type do
42
+ eq do |scope, value|
43
+ self_ref.belongs_to_many_filter(scope, value)
44
+ end
42
45
  end
43
46
  end
44
47
  end
@@ -31,16 +31,19 @@ module Graphiti
31
31
  stats[name] = {}
32
32
 
33
33
  each_calculation(name, calculation) do |calc, function|
34
- args = [@scope, name]
35
- args << @resource.context if function.arity >= 3
36
- args << @data if function.arity == 4
37
-
38
- stats[name][calc] = function.call(*args)
34
+ stats[name][calc] = calculate_stat(name, function)
39
35
  end
40
36
  end
41
37
  end
42
38
  end
43
39
 
40
+ def calculate_stat(name, function)
41
+ args = [@scope, name]
42
+ args << @resource.context if function.arity >= 3
43
+ args << @data if function.arity == 4
44
+ function.call(*args)
45
+ end
46
+
44
47
  private
45
48
 
46
49
  def each_calculation(name, calculations)
@@ -44,7 +44,7 @@ module Graphiti
44
44
  Graphiti::Errors::UnknownAttribute
45
45
 
46
46
  if raise_error?(opts[:exists])
47
- raise error_class.new(resource, name, flag, opts)
47
+ raise error_class.new(resource, name, flag, **opts)
48
48
  else
49
49
  false
50
50
  end
@@ -20,6 +20,12 @@ module Graphiti
20
20
  split = namespace.split("::")
21
21
  split[0, split.length - 1].join("::")
22
22
  end
23
+
24
+ def self.graphql_type_name(name)
25
+ name.gsub("Resource", "")
26
+ .gsub("::", "") # remove modules
27
+ .gsub(".", "__") # remove remote resource .
28
+ end
23
29
  end
24
30
  end
25
31
  end
@@ -14,6 +14,7 @@ class Graphiti::Util::Persistence
14
14
  @relationships = relationships
15
15
  @caller_model = caller_model
16
16
  @foreign_key = foreign_key
17
+ @adapter = @resource.adapter
17
18
 
18
19
  # Find the correct child resource for a given jsonapi type
19
20
  if (meta_type = @meta[:type].try(:to_sym))
@@ -43,18 +44,16 @@ class Graphiti::Util::Persistence
43
44
  #
44
45
  # @return a model instance
45
46
  def run
46
- parents = process_belongs_to(@relationships)
47
- update_foreign_key_for_parents(parents)
47
+ attributes = @adapter.persistence_attributes(self, @attributes)
48
48
 
49
- persisted = persist_object(@meta[:method], @attributes)
49
+ parents = @adapter.process_belongs_to(self, attributes)
50
+ persisted = persist_object(@meta[:method], attributes)
50
51
  @resource.decorate_record(persisted)
51
52
  assign_temp_id(persisted, @meta[:temp_id])
52
53
 
53
54
  associate_parents(persisted, parents)
54
55
 
55
- children = process_has_many(@relationships, persisted) { |x|
56
- update_foreign_key(persisted, x[:attributes], x)
57
- }
56
+ children = @adapter.process_has_many(self, persisted)
58
57
 
59
58
  associate_children(persisted, children) unless @meta[:method] == :destroy
60
59
 
@@ -69,43 +68,21 @@ class Graphiti::Util::Persistence
69
68
  persisted
70
69
  end
71
70
 
72
- private
73
-
74
- def add_hook(prc, lifecycle_event)
75
- ::Graphiti::Util::TransactionHooksRecorder.add(prc, lifecycle_event)
76
- end
77
-
78
- # The child's attributes should be modified to nil-out the
79
- # foreign_key when the parent is being destroyed or disassociated
80
- #
81
- # This is not the case for HABTM, whose "foreign key" is a join table
82
- def update_foreign_key(parent_object, attrs, x)
83
- return if x[:sideload].type == :many_to_many
71
+ def iterate(only: [], except: [])
72
+ opts = {
73
+ resource: @resource,
74
+ relationships: @relationships
75
+ }.merge(only: only, except: except)
84
76
 
85
- if [:destroy, :disassociate].include?(x[:meta][:method])
86
- if x[:sideload].polymorphic_has_one? || x[:sideload].polymorphic_has_many?
87
- attrs[:"#{x[:sideload].polymorphic_as}_type"] = nil
88
- end
89
- attrs[x[:foreign_key]] = nil
90
- update_foreign_type(attrs, x, null: true) if x[:is_polymorphic]
91
- else
92
- if x[:sideload].polymorphic_has_one? || x[:sideload].polymorphic_has_many?
93
- attrs[:"#{x[:sideload].polymorphic_as}_type"] = parent_object.class.name
94
- end
95
- attrs[x[:foreign_key]] = parent_object.send(x[:primary_key])
96
- update_foreign_type(attrs, x) if x[:is_polymorphic]
77
+ Graphiti::Util::RelationshipPayload.iterate(**opts) do |x|
78
+ yield x
97
79
  end
98
80
  end
99
81
 
100
- def update_foreign_type(attrs, x, null: false)
101
- grouping_field = x[:sideload].parent.grouper.field_name
102
- attrs[grouping_field] = null ? nil : x[:sideload].group_name
103
- end
82
+ private
104
83
 
105
- def update_foreign_key_for_parents(parents)
106
- parents.each do |x|
107
- update_foreign_key(x[:object], @attributes, x)
108
- end
84
+ def add_hook(prc, lifecycle_event)
85
+ ::Graphiti::Util::TransactionHooksRecorder.add(prc, lifecycle_event)
109
86
  end
110
87
 
111
88
  def associate_parents(object, parents)
@@ -161,35 +138,6 @@ class Graphiti::Util::Persistence
161
138
  end
162
139
  end
163
140
 
164
- def process_has_many(relationships, caller_model)
165
- [].tap do |processed|
166
- iterate(except: [:polymorphic_belongs_to, :belongs_to]) do |x|
167
- yield x
168
-
169
- x[:object] = x[:resource]
170
- .persist_with_relationships(x[:meta], x[:attributes], x[:relationships], caller_model, x[:foreign_key])
171
-
172
- processed << x
173
- end
174
- end
175
- end
176
-
177
- def process_belongs_to(relationships)
178
- [].tap do |processed|
179
- iterate(only: [:polymorphic_belongs_to, :belongs_to]) do |x|
180
- begin
181
- id = x.dig(:attributes, :id)
182
- x[:object] = x[:resource]
183
- .persist_with_relationships(x[:meta], x[:attributes], x[:relationships])
184
- processed << x
185
- rescue Graphiti::Errors::RecordNotFound
186
- path = "relationships/#{x.dig(:meta, :jsonapi_type)}"
187
- raise Graphiti::Errors::RecordNotFound.new(x[:sideload].name, id, path)
188
- end
189
- end
190
- end
191
- end
192
-
193
141
  def post_process(caller_model, processed)
194
142
  groups = processed.group_by { |x| x[:meta][:method] }
195
143
  groups.each_pair do |method, group|
@@ -205,17 +153,6 @@ class Graphiti::Util::Persistence
205
153
  object.instance_variable_set(:@_jsonapi_temp_id, temp_id)
206
154
  end
207
155
 
208
- def iterate(only: [], except: [])
209
- opts = {
210
- resource: @resource,
211
- relationships: @relationships
212
- }.merge(only: only, except: except)
213
-
214
- Graphiti::Util::RelationshipPayload.iterate(opts) do |x|
215
- yield x
216
- end
217
- end
218
-
219
156
  def metadata
220
157
  {
221
158
  method: @meta[:method],
@@ -2,13 +2,14 @@
2
2
  module Graphiti
3
3
  module Util
4
4
  class RemoteParams
5
- def self.generate(resource, query)
6
- new(resource, query).generate
5
+ def self.generate(resource, query, foreign_key)
6
+ new(resource, query, foreign_key).generate
7
7
  end
8
8
 
9
- def initialize(resource, query)
9
+ def initialize(resource, query, foreign_key)
10
10
  @resource = resource
11
11
  @query = query
12
+ @foreign_key = foreign_key
12
13
  @sorts = []
13
14
  @filters = {}
14
15
  @fields = {}
@@ -97,7 +98,11 @@ module Graphiti
97
98
  return unless fields
98
99
 
99
100
  fields.each_pair do |type, attrs|
100
- @fields[type] = attrs.join(",")
101
+ all_attrs = attrs
102
+ if @foreign_key
103
+ all_attrs |= [@foreign_key]
104
+ end
105
+ @fields[type] = all_attrs.join(",")
101
106
  end
102
107
  end
103
108
 
@@ -44,6 +44,7 @@ module Graphiti
44
44
  model.delete_field(:_relationships)
45
45
  # If this isn't set, Array(resources) will return []
46
46
  # This is important, because jsonapi-serializable makes this call
47
+ # To see the test failure, remote resource position > department
47
48
  model.instance_variable_set(:@__graphiti_resource, 1)
48
49
  model.instance_variable_set(:@__graphiti_serializer, serializer)
49
50
  end
@@ -12,18 +12,20 @@ module Graphiti
12
12
  def apply
13
13
  return unless readable?
14
14
 
15
+ remove_guard if previously_guarded?
16
+
15
17
  if @name == :id
16
18
  @serializer.id(&proc)
17
19
  elsif @attr[:proc]
18
20
  @serializer.send(_method, @name, serializer_options, &proc)
19
21
  elsif @serializer.attribute_blocks[@name].nil?
20
22
  @serializer.send(_method, @name, serializer_options, &proc)
23
+ elsif @serializer.send(applied_method).include?(@name)
24
+ @serializer.field_condition_blocks[@name] = guard if guard?
21
25
  else
22
- unless @serializer.send(applied_method).include?(@name)
23
- inner = @serializer.attribute_blocks.delete(@name)
24
- wrapped = wrap_proc(inner)
25
- @serializer.send(_method, @name, serializer_options, &wrapped)
26
- end
26
+ inner = @serializer.attribute_blocks.delete(@name)
27
+ wrapped = wrap_proc(inner)
28
+ @serializer.send(_method, @name, serializer_options, &wrapped)
27
29
  end
28
30
 
29
31
  existing = @serializer.send(applied_method)
@@ -32,6 +34,14 @@ module Graphiti
32
34
 
33
35
  private
34
36
 
37
+ def previously_guarded?
38
+ @serializer.field_condition_blocks[@name]
39
+ end
40
+
41
+ def remove_guard
42
+ @serializer.field_condition_blocks.delete(@name)
43
+ end
44
+
35
45
  def applied_method
36
46
  if extra?
37
47
  :extra_attributes_applied_via_resource
@@ -55,14 +65,22 @@ module Graphiti
55
65
  def guard
56
66
  method_name = @attr[:readable]
57
67
  instance = @resource.new
68
+ attribute = @name.to_s
69
+ resource_class = @resource
58
70
 
59
71
  -> {
60
72
  method = instance.method(method_name)
61
- if method.arity.zero?
73
+ result = if method.arity.zero?
62
74
  instance.instance_eval(&method_name)
63
- else
75
+ elsif method.arity == 1
64
76
  instance.instance_exec(@object, &method)
77
+ else
78
+ instance.instance_exec(@object, attribute, &method)
79
+ end
80
+ if Graphiti.context[:graphql] && !result
81
+ raise ::Graphiti::Errors::UnreadableAttribute.new(resource_class, attribute)
65
82
  end
83
+ result
66
84
  }
67
85
  end
68
86
 
@@ -1,3 +1,3 @@
1
1
  module Graphiti
2
- VERSION = "1.2.27"
2
+ VERSION = "1.2.32"
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.2.27
4
+ version: 1.2.32
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lee Richmond
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-09-03 00:00:00.000000000 Z
11
+ date: 2021-02-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jsonapi-serializable
@@ -204,7 +204,7 @@ dependencies:
204
204
  - - '='
205
205
  - !ruby/object:Gem::Version
206
206
  version: 1.0.beta.4
207
- description:
207
+ description:
208
208
  email:
209
209
  - richmolj@gmail.com
210
210
  executables:
@@ -266,6 +266,7 @@ files:
266
266
  - lib/graphiti/adapters/active_record/many_to_many_sideload.rb
267
267
  - lib/graphiti/adapters/graphiti_api.rb
268
268
  - lib/graphiti/adapters/null.rb
269
+ - lib/graphiti/adapters/persistence/associations.rb
269
270
  - lib/graphiti/cli.rb
270
271
  - lib/graphiti/configuration.rb
271
272
  - lib/graphiti/context.rb
@@ -341,15 +342,18 @@ homepage: https://github.com/graphiti-api/graphiti
341
342
  licenses:
342
343
  - MIT
343
344
  metadata: {}
344
- post_install_message:
345
+ post_install_message:
345
346
  rdoc_options: []
346
347
  require_paths:
347
348
  - lib
348
349
  required_ruby_version: !ruby/object:Gem::Requirement
349
350
  requirements:
350
- - - "~>"
351
+ - - ">="
351
352
  - !ruby/object:Gem::Version
352
353
  version: '2.3'
354
+ - - "<"
355
+ - !ruby/object:Gem::Version
356
+ version: '3.1'
353
357
  required_rubygems_version: !ruby/object:Gem::Requirement
354
358
  requirements:
355
359
  - - ">="
@@ -357,7 +361,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
357
361
  version: '0'
358
362
  requirements: []
359
363
  rubygems_version: 3.0.6
360
- signing_key:
364
+ signing_key:
361
365
  specification_version: 4
362
366
  summary: Easily build jsonapi.org-compatible APIs
363
367
  test_files: []