graphiti 2.0.0.beta.9 → 2.0.0.beta.10

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: a61d00c6968d2dc24afb8986d7b3b6051394ea73ac5d5a8104f1296f650fef47
4
- data.tar.gz: bf367c2e3ff33e4240a3f11e3fbae780c75b7f727863e61fc0c546ae9cdfb5fe
3
+ metadata.gz: b1e331ab2dea47987ea47260eb0890c9a953d3435bba2e7ef37cbfbbc83fe5ab
4
+ data.tar.gz: 13a7483ead9f4ad5ed69390ff6a4a122a819813c2147c0a56dceebdfebc57e17
5
5
  SHA512:
6
- metadata.gz: 4bb22545b0a2b590fb07a237857407a5a50ac2423dd33a5621af164d93b4d36237f54339fb083895e6d349ae64da3cf9cc2e24c363f63478cea691622f130331
7
- data.tar.gz: 9da999b97fa15b04ca97420d6a6942c5d32b70bd0e8f27e33e01b8861cdd0206342d6afd11db15eb0c1d445beda4ce1f6a5ff5cceffcb54d5917e3a185cdc16a
6
+ metadata.gz: cca252389b52b5a2983407b4eff7cf7fe8b49759b478228f9d78303522bc40f2948a1784d6c863373f10521dc80b61d35f201009e830d8a968cc989b37d2ad81
7
+ data.tar.gz: 3fdfda722d09ddea6c366ca4102258bdea6d36c923be729ae141063e7e62b43ae0730f30a6422ae4448f858a5c1d4269f80180279d7939ce050c88008613cedb
data/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  graphiti changelog
2
2
 
3
+ # [2.0.0-beta.10](https://github.com/graphiti-api/graphiti/compare/v2.0.0-beta.9...v2.0.0-beta.10) (2026-08-24)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * apply sideloads at definition once setup! has run ([39ef393](https://github.com/graphiti-api/graphiti/commit/39ef39383520816319282854c0ae99677bcd5127)), closes [#524](https://github.com/graphiti-api/graphiti/issues/524)
9
+ * give client-caused errors real detail and stop 500ing on bad query params ([35acc3e](https://github.com/graphiti-api/graphiti/commit/35acc3e6c78f68eeeb05728ea16264b136455c30)), closes [#531](https://github.com/graphiti-api/graphiti/issues/531)
10
+ * let an abstract resource declare relationships ([6036392](https://github.com/graphiti-api/graphiti/commit/6036392ebd611c7ed492beb3fe5754433eac9ffd)), closes [#453](https://github.com/graphiti-api/graphiti/issues/453)
11
+ * reject a non-object data payload instead of raising TypeError ([51603b8](https://github.com/graphiti-api/graphiti/commit/51603b854270715b80a75d9aa05a4020e6bbdce5))
12
+ * Update some missing sections and terms after renames, register unsupported pagination error ([39cff19](https://github.com/graphiti-api/graphiti/commit/39cff19f6f1f73126514c8b18234a18ad5eb7379))
13
+
14
+
15
+ ### Features
16
+
17
+ * carry CurrentAttributes into concurrent sideloads ([7092096](https://github.com/graphiti-api/graphiti/commit/7092096f6d99ad277cc56817c616d946f655d55b))
18
+ * fold allow_nil and deny_empty into one blanks: filter option ([a108403](https://github.com/graphiti-api/graphiti/commit/a108403e5cd87fd2fdf177da30c4c4f97b9fe750))
19
+ * move link rendering onto the resource as page_links and relationship_links ([57878d1](https://github.com/graphiti-api/graphiti/commit/57878d14fdfc4741e8d418c8edbc4a8687335843))
20
+ * move typecast_reads onto the resource and let the railtie own Rails config ([236e970](https://github.com/graphiti-api/graphiti/commit/236e9706d7d8f08e9640c5c22ac596e2f7466849))
21
+ * one link mode per relationship, cascading from relationship_links ([f0e1da1](https://github.com/graphiti-api/graphiti/commit/f0e1da1e13a8d12b47458495b378779f5687ffe2))
22
+ * point connection pool timeouts at the sizing formula and add an audit task to check ([6e017c6](https://github.com/graphiti-api/graphiti/commit/6e017c60fd45338e4e6c8527adebfd46be4bfd8d))
23
+ * split validate_endpoints into validate_requests and validate_links ([100a370](https://github.com/graphiti-api/graphiti/commit/100a370deec6a0e6ae165bdf77e50b6b14d068d3))
24
+
3
25
  # [2.0.0-beta.9](https://github.com/graphiti-api/graphiti/compare/v2.0.0-beta.8...v2.0.0-beta.9) (2026-08-22)
4
26
 
5
27
 
@@ -33,6 +33,30 @@ module Graphiti
33
33
  methods.any? { |m| actions.include?(m) }
34
34
  end
35
35
 
36
+ def resource_setting_groups
37
+ name_width = Graphiti::Resource::SETTINGS.keys.map(&:length).max
38
+ assignments = Graphiti::Resource::SETTINGS.to_h do |name, setting|
39
+ value = setting[:format] || setting[:default].inspect
40
+ [name, "self.#{name.to_s.ljust(name_width)} = #{value}"]
41
+ end
42
+ hint_column = assignments.values.map(&:length).max + 2
43
+
44
+ Graphiti::Resource::SETTING_GROUPS.transform_values do |settings|
45
+ settings.map do |name, setting|
46
+ hint = resource_setting_hint(setting)
47
+ hint ? "#{assignments[name].ljust(hint_column)}# #{hint}" : assignments[name]
48
+ end
49
+ end
50
+ end
51
+
52
+ def resource_setting_hint(setting)
53
+ return setting[:note] if setting[:note]
54
+ return unless setting[:values]
55
+
56
+ setting[:values].map(&:inspect)
57
+ .to_sentence(two_words_connector: " or ", last_word_connector: ", or ")
58
+ end
59
+
36
60
  def graphiti_config
37
61
  File.exist?(".graphiticfg.yml") ? YAML.load_file(".graphiticfg.yml") : {}
38
62
  end
@@ -14,21 +14,14 @@ class ApplicationResource < Graphiti::Resource
14
14
  self.endpoint_namespace = '<%= api_namespace %>'
15
15
  <%- unless omit_comments? -%>
16
16
 
17
- # Defaults every Resource inherits. These are already the values shown;
18
- # uncomment to change them API-wide, or set them on individual Resources.
19
- # See https://graphiti.dev/concepts/resources
20
- #
21
- # self.attributes_readable_by_default = true
22
- # self.attributes_writable_by_default = true
23
- # self.attributes_sortable_by_default = true
24
- # self.attributes_filterable_by_default = true
25
- # self.attributes_schema_by_default = true
26
- # self.relationships_readable_by_default = true
27
- # self.relationships_writable_by_default = true
28
- # self.belongs_to_resource_ids_by_default = :foreign_key
29
- # self.filters_accept_nil_by_default = false
30
- # self.filters_deny_empty_by_default = false
31
- # self.default_page_size = 20
32
- # self.max_page_size = 1_000
17
+ # Defaults every Resource inherits. Uncomment to change them API-wide, or set
18
+ # them on individual Resources. See https://graphiti.dev/concepts/resources
19
+ <%- resource_setting_groups.each_pair do |group, lines| -%>
20
+
21
+ # <%= group.to_s.humanize %>
22
+ <%- lines.each do |line| -%>
23
+ # <%= line %>
24
+ <%- end -%>
25
+ <%- end -%>
33
26
  <%- end -%>
34
27
  end
@@ -24,10 +24,6 @@ module Graphiti
24
24
 
25
25
  attr_accessor :respond_to
26
26
  attr_accessor :context_for_endpoint
27
- attr_accessor :links_on_demand
28
- attr_accessor :pagination_links_on_demand
29
- attr_accessor :pagination_links
30
- attr_accessor :typecast_reads
31
27
  attr_accessor :raise_on_missing_sidepost
32
28
  attr_accessor :before_sideload
33
29
 
@@ -44,32 +40,12 @@ module Graphiti
44
40
  @concurrency = false
45
41
  @concurrency_max_threads = 4
46
42
  @respond_to = [:json, :jsonapi, :xml]
47
- @links_on_demand = false
48
- @pagination_links_on_demand = false
49
- @pagination_links = false
50
- @typecast_reads = true
51
43
  @raise_on_missing_sidepost = true
52
44
  @cache_rendering = false
53
45
  self.debug = ENV.fetch("GRAPHITI_DEBUG", true)
54
46
  self.debug_models = ENV.fetch("GRAPHITI_DEBUG_MODELS", false)
55
47
 
56
48
  @uri_decoder = infer_uri_decoder
57
-
58
- # FIXME: Don't duplicate the Railtie's initializer
59
- if defined?(::Rails.root) && (root = ::Rails.root)
60
- config_file = root.join(".graphiticfg.yml")
61
- if config_file.exist?
62
- cfg = YAML.load_file(config_file)
63
- @schema_path = root.join("public#{cfg["namespace"]}/schema.json")
64
- else
65
- @schema_path = root.join("public/schema.json")
66
- end
67
-
68
- if (logger = ::Rails.logger)
69
- self.debug = logger.debug? && debug
70
- Graphiti.logger = logger
71
- end
72
- end
73
49
  end
74
50
 
75
51
  def cache_rendering?
@@ -111,6 +87,48 @@ module Graphiti
111
87
  send(:"#{key}=", original)
112
88
  end
113
89
 
90
+ def typecast_reads
91
+ Resource.typecast_reads
92
+ end
93
+
94
+ def typecast_reads=(val)
95
+ Resource.typecast_reads = val
96
+ end
97
+
98
+ def links_on_demand
99
+ Resource.relationship_links == :on_demand
100
+ end
101
+
102
+ def links_on_demand=(val)
103
+ if val
104
+ Resource.relationship_links = :on_demand
105
+ elsif Resource.relationship_links == :on_demand
106
+ Resource.relationship_links = true
107
+ end
108
+ end
109
+
110
+ def pagination_links
111
+ Resource.page_links == true
112
+ end
113
+
114
+ def pagination_links=(val)
115
+ return if Resource.page_links == :on_demand
116
+
117
+ Resource.page_links = !!val
118
+ end
119
+
120
+ def pagination_links_on_demand
121
+ Resource.page_links == :on_demand
122
+ end
123
+
124
+ def pagination_links_on_demand=(val)
125
+ if val
126
+ Resource.page_links = :on_demand
127
+ elsif Resource.page_links == :on_demand
128
+ Resource.page_links = false
129
+ end
130
+ end
131
+
114
132
  def uri_decoder=(decoder)
115
133
  unless decoder.respond_to?(:call)
116
134
  raise "uri_decoder must respond to `call`."
@@ -141,4 +159,17 @@ module Graphiti
141
159
 
142
160
  msg = "Use `config.graphiti.respond_to_formats`"
143
161
  DEPRECATOR.deprecate_methods(Configuration, respond_to: msg, "respond_to=": msg)
162
+
163
+ relationship_msg = "Set `self.relationship_links` (true, false, or :on_demand) on your resource"
164
+ pagination_msg = "Set `self.page_links` (true, false, or :on_demand) on your resource"
165
+ typecast_msg = "Set `self.typecast_reads` on your resource"
166
+ DEPRECATOR.deprecate_methods(Configuration,
167
+ typecast_reads: typecast_msg,
168
+ "typecast_reads=": typecast_msg,
169
+ links_on_demand: relationship_msg,
170
+ "links_on_demand=": relationship_msg,
171
+ pagination_links: pagination_msg,
172
+ "pagination_links=": pagination_msg,
173
+ pagination_links_on_demand: pagination_msg,
174
+ "pagination_links_on_demand=": pagination_msg)
144
175
  end
@@ -6,7 +6,7 @@ module Graphiti
6
6
  end
7
7
 
8
8
  def links?
9
- @proxy.query.pagination_links? && @proxy.data.present?
9
+ @proxy.query.page_links? && @proxy.data.present?
10
10
  end
11
11
 
12
12
  def links
@@ -104,7 +104,7 @@ module Graphiti
104
104
 
105
105
  def page_size
106
106
  @page_size ||= (page_param[:size] ||
107
- @proxy.resource.default_page_size ||
107
+ @proxy.resource.page_default_size ||
108
108
  Graphiti::Scoping::Paginate::DEFAULT_PAGE_SIZE).to_i
109
109
  end
110
110
 
@@ -221,7 +221,7 @@ module Graphiti
221
221
 
222
222
  Make sure the endpoint "#{@sideload.resource.endpoint[:full_path]}" exists with action #{@action.inspect}, or customize the endpoint for #{@sideload.resource.class.name}.
223
223
 
224
- If you do not wish to generate a link, pass link: false or set self.autolink = false.
224
+ If you do not wish to generate a link, pass link: false or set self.relationship_links = false. To keep the link and stop checking it, set self.validate_links = false.
225
225
  MSG
226
226
  end
227
227
  end
@@ -255,6 +255,8 @@ module Graphiti
255
255
  Graphiti.config.context_for_endpoint must be set to enable link generation:
256
256
 
257
257
  Graphiti.config.context_for_endpoint = ->(path, action) { ... }
258
+
259
+ Or set self.validate_links = false to render links without checking them.
258
260
  MSG
259
261
  end
260
262
  end
@@ -445,9 +447,9 @@ module Graphiti
445
447
 
446
448
  secondary_endpoint '/my_url', [:index, :update]
447
449
 
448
- Or disable endpoint validation for this resource:
450
+ Or disable request validation for this resource:
449
451
 
450
- self.validate_endpoints = false
452
+ self.validate_requests = false
451
453
 
452
454
  See https://graphiti.dev/concepts/links for more information.
453
455
 
@@ -708,6 +710,36 @@ module Graphiti
708
710
  end
709
711
  end
710
712
 
713
+ class InvalidFilterBlanks < Base
714
+ def initialize(resource_class, attribute, value)
715
+ @resource_class = resource_class
716
+ @attribute = attribute
717
+ @value = value
718
+ end
719
+
720
+ def message
721
+ <<~MSG
722
+ #{@resource_class.name}: #{@attribute} must be one of :as_literal, :as_nil, or :reject. Got #{@value.inspect}.
723
+
724
+ :as_literal - "null" and "" reach the filter as strings (default)
725
+ :as_nil - "null" becomes nil, so the filter can query for NULL
726
+ :reject - a blank value raises InvalidFilterValue
727
+ MSG
728
+ end
729
+ end
730
+
731
+ class InvalidLinkRendering < Base
732
+ def initialize(resource_class, attribute, value)
733
+ @resource_class = resource_class
734
+ @attribute = attribute
735
+ @value = value
736
+ end
737
+
738
+ def message
739
+ "#{@resource_class.name}: #{@attribute} must be true, false, or :on_demand. Got #{@value.inspect}."
740
+ end
741
+ end
742
+
711
743
  class UnselectedForeignKey < Base
712
744
  def initialize(resource_class, sideload, model)
713
745
  @resource_class = resource_class
@@ -2,6 +2,9 @@ require "digest"
2
2
 
3
3
  module Graphiti
4
4
  class Query
5
+ TRUTHY_LINKS = [true, "true"].freeze
6
+ LINKLESS_FORMATS = [:json, :xml, "json", "xml"].freeze
7
+
5
8
  attr_reader :resource, :association_name, :params, :action
6
9
 
7
10
  def initialize(resource, params, *positional, association_name: nil, nested_include: nil, parents: [], action: nil)
@@ -40,25 +43,39 @@ module Graphiti
40
43
  !association?
41
44
  end
42
45
 
43
- def links?
44
- return false if [:json, :xml, "json", "xml"].include?(params[:format])
45
- if Graphiti.config.links_on_demand
46
- [true, "true"].include?(@params[:links])
47
- else
48
- true
49
- end
46
+ def links_requested?
47
+ return @links_requested if defined?(@links_requested)
48
+
49
+ @links_requested = TRUTHY_LINKS.include?(@params[:links])
50
+ end
51
+
52
+ def suppress_links?
53
+ return @suppress_links if defined?(@suppress_links)
54
+
55
+ @suppress_links = LINKLESS_FORMATS.include?(params[:format])
50
56
  end
51
57
 
52
- def pagination_links?
58
+ def render_link?(mode)
59
+ return false if suppress_links?
60
+
61
+ (mode == :on_demand) ? links_requested? : mode == true
62
+ end
63
+
64
+ def page_links?
53
65
  if action == :find
54
66
  false
55
- elsif Graphiti.config.pagination_links_on_demand
56
- [true, "true"].include?(@params[:pagination_links])
67
+ elsif @resource.page_links == :on_demand
68
+ page_links_requested?
57
69
  else
58
- Graphiti.config.pagination_links
70
+ @resource.page_links
59
71
  end
60
72
  end
61
73
 
74
+ def page_links_requested?
75
+ [@params[:page_links], @params[:pagination_links]]
76
+ .any? { |value| [true, "true"].include?(value) }
77
+ end
78
+
62
79
  def debug_requested?
63
80
  !!@params[:debug]
64
81
  end
@@ -267,8 +284,8 @@ module Graphiti
267
284
  def query_cache_key
268
285
  attrs = {extra_fields: extra_fields,
269
286
  fields: fields,
270
- links: links?,
271
- pagination_links: pagination_links?,
287
+ links: suppress_links? ? :none : links_requested?,
288
+ page_links: page_links?,
272
289
  format: params[:format]}
273
290
 
274
291
  Digest::SHA1.hexdigest(attrs.to_s)
@@ -387,5 +404,9 @@ module Graphiti
387
404
  action
388
405
  end
389
406
  end
407
+
408
+ alias_method :pagination_links?, :page_links?
409
+ DEPRECATOR.deprecate_methods(self,
410
+ pagination_links?: "Use `page_links?`")
390
411
  end
391
412
  end
@@ -30,14 +30,34 @@ module Graphiti
30
30
 
31
31
  register_exception Graphiti::Errors::InvalidRequest,
32
32
  status: 400, handler: Graphiti::Rails::InvalidRequestHandler
33
- register_exception Graphiti::Errors::ConflictRequest,
34
- status: 409, handler: Graphiti::Rails::ConflictRequestHandler
35
- register_exception Graphiti::Errors::RecordNotFound,
36
- status: 404, handler: Graphiti::Rails::ExceptionHandler
37
33
  register_exception Graphiti::Errors::RemoteWrite,
38
- status: 400, handler: Graphiti::Rails::ExceptionHandler
34
+ status: 400, detail: :exception, handler: Graphiti::Rails::ExceptionHandler
39
35
  register_exception Graphiti::Errors::SingularSideload,
40
- status: 400, handler: Graphiti::Rails::ExceptionHandler
36
+ status: 400, detail: :exception, handler: Graphiti::Rails::ExceptionHandler
37
+ register_exception Graphiti::Errors::InvalidInclude,
38
+ status: 400, detail: :exception, handler: Graphiti::Rails::ExceptionHandler
39
+ register_exception Graphiti::Errors::UnsupportedSort,
40
+ status: 400, detail: :exception, handler: Graphiti::Rails::ExceptionHandler
41
+ register_exception Graphiti::Errors::UnsupportedOperator,
42
+ status: 400, detail: :exception, handler: Graphiti::Rails::ExceptionHandler
43
+ register_exception Graphiti::Errors::InvalidFilterValue,
44
+ status: 400, detail: :exception, handler: Graphiti::Rails::ExceptionHandler
45
+ register_exception Graphiti::Errors::RequiredFilter,
46
+ status: 400, detail: :exception, handler: Graphiti::Rails::ExceptionHandler
47
+ register_exception Graphiti::Errors::UnsupportedPageSize,
48
+ status: 400, detail: :exception, handler: Graphiti::Rails::ExceptionHandler
49
+ register_exception Graphiti::Errors::UnsupportedBeforeCursor,
50
+ status: 400, detail: :exception, handler: Graphiti::Rails::ExceptionHandler
51
+ register_exception Graphiti::Errors::UnsupportedPagination,
52
+ status: 400, detail: :exception, handler: Graphiti::Rails::ExceptionHandler
53
+ register_exception Graphiti::Errors::FilterGroupMissingRequiredFilters,
54
+ status: 400, detail: :exception, handler: Graphiti::Rails::ExceptionHandler
55
+
56
+ register_exception Graphiti::Errors::RecordNotFound,
57
+ status: 404, detail: :exception, handler: Graphiti::Rails::ExceptionHandler
58
+
59
+ register_exception Graphiti::Errors::ConflictRequest,
60
+ status: 409, handler: Graphiti::Rails::ConflictRequestHandler
41
61
  end
42
62
  end
43
63
  end
@@ -21,6 +21,23 @@ module Graphiti
21
21
  ::Rails.application.config.action_controller.perform_caching = false
22
22
  end
23
23
 
24
+ # The pool and RAILS_MAX_THREADS are read from this environment, and production may size both differently.
25
+ def connection_pool_advisory
26
+ return unless defined?(ActiveRecord::Base)
27
+
28
+ pool = ActiveRecord::Base.connection_db_config.pool
29
+ threads = ENV.fetch("RAILS_MAX_THREADS", 5).to_i
30
+ sideload_threads = Graphiti.config.concurrency_max_threads
31
+ needed = threads + sideload_threads + 1
32
+ return if pool.nil? || pool >= needed
33
+
34
+ "WARNING database.yml pool is #{pool} in this environment. With concurrency on (the production default), " \
35
+ "each sideload holds its own connection, so pool should be at least " \
36
+ "web threads (#{threads}) + concurrency_max_threads (#{sideload_threads}) + 1 = #{needed}. " \
37
+ "If production sizes these differently, check the numbers there. " \
38
+ "See graphiti.dev/concepts/resources#concurrency-pool-sizing."
39
+ end
40
+
24
41
  def make_request(path, debug = false)
25
42
  if path.split("/").length == 2
26
43
  path = "#{ApplicationResource.endpoint_namespace}#{path}"
@@ -2,7 +2,7 @@ module Graphiti
2
2
  module RequestValidators
3
3
  class UpdateValidator < Validator
4
4
  def validate
5
- if required_payload? && payload_matches_endpoint?
5
+ if validate_data_shape && required_payload? && payload_matches_endpoint?
6
6
  super
7
7
  else
8
8
  false
@@ -17,6 +17,7 @@ module Graphiti
17
17
  # In the future these should have their own validation logic, but
18
18
  # for now we can just bypass
19
19
  return errors.blank? unless @params.has_key?(:data)
20
+ return false unless validate_data_shape
20
21
 
21
22
  resource = @root_resource
22
23
  if @params[:data].has_key?(:type)
@@ -56,6 +57,14 @@ module Graphiti
56
57
  @errors.add(:page, :invalid, message: "must be an object")
57
58
  end
58
59
 
60
+ def validate_data_shape
61
+ data = @params[:data]
62
+ return true if data.nil? || data.respond_to?(:each_pair)
63
+
64
+ @errors.add(:data, :invalid, message: "must be an object")
65
+ false
66
+ end
67
+
59
68
  def process_relationships(resource, relationships, payload_path)
60
69
  relationships.each_key do |name|
61
70
  unless resource.class.sideload(name.to_sym)
@@ -4,16 +4,75 @@ module Graphiti
4
4
  extend ActiveSupport::Concern
5
5
 
6
6
  DEFAULT_MAX_PAGE_SIZE = 1_000
7
+ LINK_MODES = [true, false, :on_demand].freeze
8
+ BELONGS_TO_RESOURCE_IDS_MODES = [:foreign_key, :always, :never].freeze
9
+ BLANK_MODES = [:as_literal, :as_nil, :reject].freeze
10
+
11
+ # Grouped for the ApplicationResource the install generator writes.
12
+ SETTING_GROUPS = { # :nodoc:
13
+ attributes: {
14
+ attributes_readable_by_default: {default: true},
15
+ attributes_writable_by_default: {default: true},
16
+ attributes_sortable_by_default: {default: true},
17
+ attributes_filterable_by_default: {default: true},
18
+ attributes_schema_by_default: {default: true},
19
+ typecast_reads: {default: true}
20
+ },
21
+ relationships: {
22
+ relationships_readable_by_default: {default: true},
23
+ relationships_writable_by_default: {default: true},
24
+ belongs_to_resource_ids_by_default: {
25
+ default: :foreign_key,
26
+ values: BELONGS_TO_RESOURCE_IDS_MODES,
27
+ invalid: ->(klass, value) { Errors::InvalidBelongsToResourceIds.new(klass, value) }
28
+ }
29
+ },
30
+ filters: {
31
+ filters_blanks_by_default: {
32
+ default: :as_literal,
33
+ values: BLANK_MODES,
34
+ invalid: ->(klass, value) { Errors::InvalidFilterBlanks.new(klass, :filters_blanks_by_default, value) }
35
+ }
36
+ },
37
+ sorting: {
38
+ default_sort: {default: nil, note: "per resource, e.g. [{id: :desc}]"}
39
+ },
40
+ pagination: {
41
+ page_default_size: {default: nil, note: "unset falls back to 20"},
42
+ page_max_size: {default: DEFAULT_MAX_PAGE_SIZE, format: "1_000"},
43
+ page_cursors: {default: false, note: "render cursors for pagination"},
44
+ page_links: {
45
+ default: false,
46
+ values: LINK_MODES,
47
+ invalid: ->(klass, value) { Errors::InvalidLinkRendering.new(klass, :page_links, value) }
48
+ }
49
+ },
50
+ endpoints: {
51
+ validate_requests: {default: true, note: "refuse requests to undeclared endpoints"}
52
+ },
53
+ links: {
54
+ relationship_links: {
55
+ default: true,
56
+ values: LINK_MODES,
57
+ invalid: ->(klass, value) { Errors::InvalidLinkRendering.new(klass, :relationship_links, value) }
58
+ },
59
+ validate_links: {default: true, note: "refuse to render links to unroutable endpoints"}
60
+ }
61
+ }.freeze
62
+
63
+ SETTINGS = SETTING_GROUPS.values.reduce(:merge).freeze # :nodoc:
7
64
 
8
65
  module Overrides
9
- BELONGS_TO_RESOURCE_IDS_VALUES = [:foreign_key, :always, :never].freeze
66
+ SETTINGS.each_pair do |name, setting|
67
+ next unless setting[:values]
10
68
 
11
- def belongs_to_resource_ids_by_default=(val)
12
- unless BELONGS_TO_RESOURCE_IDS_VALUES.include?(val)
13
- raise Errors::InvalidBelongsToResourceIds.new(self, val)
14
- end
69
+ define_method(:"#{name}=") do |val|
70
+ unless setting[:values].include?(val)
71
+ raise setting[:invalid].call(self, val)
72
+ end
15
73
 
16
- super
74
+ super(val)
75
+ end
17
76
  end
18
77
 
19
78
  def serializer=(val)
@@ -95,51 +154,37 @@ module Graphiti
95
154
  :polymorphic,
96
155
  :polymorphic_child,
97
156
  :serializer,
98
- :default_page_size,
99
- :default_sort,
100
- :max_page_size,
101
- :attributes_readable_by_default,
102
- :attributes_writable_by_default,
103
- :attributes_sortable_by_default,
104
- :attributes_filterable_by_default,
105
- :attributes_schema_by_default,
106
- :relationships_readable_by_default,
107
- :relationships_writable_by_default,
108
- :belongs_to_resource_ids_by_default,
109
- :filters_accept_nil_by_default,
110
- :filters_deny_empty_by_default,
111
157
  :graphql_entrypoint,
112
- :cursor_paginatable
158
+ *SETTINGS.keys
113
159
 
114
160
  class << self
115
161
  prepend Overrides
116
162
  end
117
163
 
164
+ SETTINGS.each_pair do |name, setting|
165
+ public_send(:"#{name}=", setting[:default])
166
+ end
167
+
118
168
  def self.inherited(klass)
119
169
  super
120
170
  klass.config = Util::Hash.deep_dup(config)
121
171
  klass.adapter ||= Adapters::Abstract
122
- klass.max_page_size ||= DEFAULT_MAX_PAGE_SIZE
123
172
  # re-assigning causes a new Class.new
124
173
  klass.serializer = (klass.serializer || klass.infer_serializer_superclass)
125
174
  klass.type ||= klass.infer_type
126
175
  klass.graphql_entrypoint = klass.type.to_s.pluralize.to_sym
127
- default(klass, :attributes_readable_by_default, true)
128
- default(klass, :attributes_writable_by_default, true)
129
- default(klass, :attributes_sortable_by_default, true)
130
- default(klass, :attributes_filterable_by_default, true)
131
- default(klass, :attributes_schema_by_default, true)
132
- default(klass, :relationships_readable_by_default, true)
133
- default(klass, :relationships_writable_by_default, true)
134
- default(klass, :belongs_to_resource_ids_by_default, :foreign_key)
135
- default(klass, :filters_accept_nil_by_default, false)
136
- default(klass, :filters_deny_empty_by_default, false)
137
-
138
176
  unless klass.config[:attributes][:id]
139
177
  klass.attribute :id, :integer_id
140
178
  end
141
179
  klass.stat total: [:count]
142
180
 
181
+ # An abstract parent has no serializer for its sideloads, so the subclass applies them here.
182
+ if abstract_class?
183
+ klass.config[:sideloads].each_pair do |name, sideload|
184
+ klass.apply_sideload_to_serializer(name) if klass.eagerly_apply_sideload?(sideload)
185
+ end
186
+ end
187
+
143
188
  if defined?(::Rails) && ::Rails.env.development?
144
189
  # Avoid adding dupe resources when re-autoloading
145
190
  Graphiti.resources.reject! { |r| r.name == klass.name }
@@ -149,6 +194,52 @@ module Graphiti
149
194
  end
150
195
 
151
196
  class_methods do
197
+ # Deprecated. Both folded into filters_blanks_by_default. Remove in 3.0.
198
+ def filters_accept_nil_by_default
199
+ filters_blanks_by_default == :as_nil
200
+ end
201
+
202
+ def filters_accept_nil_by_default=(val)
203
+ self.filters_blanks_by_default = val ? :as_nil : :as_literal
204
+ end
205
+
206
+ def filters_deny_empty_by_default
207
+ filters_blanks_by_default == :reject
208
+ end
209
+
210
+ def filters_deny_empty_by_default=(val)
211
+ self.filters_blanks_by_default = val ? :reject : :as_literal
212
+ end
213
+
214
+ # Deprecated. Renamed to the page_ family. Remove in 3.0.
215
+ def default_page_size
216
+ page_default_size
217
+ end
218
+
219
+ def default_page_size=(val)
220
+ self.page_default_size = val
221
+ end
222
+
223
+ def max_page_size
224
+ page_max_size
225
+ end
226
+
227
+ def max_page_size=(val)
228
+ self.page_max_size = val
229
+ end
230
+
231
+ def cursor_paginatable
232
+ page_cursors
233
+ end
234
+
235
+ def cursor_paginatable=(val)
236
+ self.page_cursors = val
237
+ end
238
+
239
+ def cursor_paginatable?
240
+ !!page_cursors
241
+ end
242
+
152
243
  def get_attr!(name, flag, opts = {})
153
244
  opts[:raise_error] = true
154
245
  get_attr(name, flag, opts)
@@ -205,14 +296,6 @@ module Graphiti
205
296
  serializer_class
206
297
  end
207
298
 
208
- def default(object, attr, value)
209
- prior = object.send(attr)
210
- unless prior || prior == false
211
- object.send(:"#{attr}=", value)
212
- end
213
- end
214
- private :default
215
-
216
299
  def config
217
300
  @config ||=
218
301
  {
@@ -332,5 +415,20 @@ module Graphiti
332
415
  self.class.default_filters
333
416
  end
334
417
  end
418
+
419
+ blanks_msg = "Use `self.filters_blanks_by_default` (:as_literal, :as_nil, or :reject)"
420
+ page_msg = "Use `self.page_default_size`, `self.page_max_size` and `self.page_cursors`"
421
+ DEPRECATOR.deprecate_methods(Configuration::ClassMethods,
422
+ filters_accept_nil_by_default: blanks_msg,
423
+ "filters_accept_nil_by_default=": blanks_msg,
424
+ filters_deny_empty_by_default: blanks_msg,
425
+ "filters_deny_empty_by_default=": blanks_msg,
426
+ default_page_size: page_msg,
427
+ "default_page_size=": page_msg,
428
+ max_page_size: page_msg,
429
+ "max_page_size=": page_msg,
430
+ cursor_paginatable: page_msg,
431
+ "cursor_paginatable=": page_msg,
432
+ cursor_paginatable?: page_msg)
335
433
  end
336
434
  end
@@ -40,8 +40,7 @@ module Graphiti
40
40
  required: required,
41
41
  schema: schema,
42
42
  operators: operators.to_hash,
43
- allow_nil: opts.fetch(:allow_nil, filters_accept_nil_by_default),
44
- deny_empty: opts.fetch(:deny_empty, filters_deny_empty_by_default)
43
+ blanks: blanks_for(name, opts)
45
44
  }
46
45
  elsif (type = args[0])
47
46
  attribute name, type, only: [:filterable], allow: opts[:allow]
@@ -206,6 +205,26 @@ module Graphiti
206
205
  end
207
206
  end
208
207
  private :relationship_option
208
+
209
+ def blanks_for(name, opts)
210
+ if opts.key?(:deny_empty)
211
+ DEPRECATOR.warn("The deny_empty: filter option is deprecated. Use blanks: :reject.")
212
+ return :reject if opts[:deny_empty]
213
+ end
214
+
215
+ if opts.key?(:allow_nil)
216
+ DEPRECATOR.warn("The allow_nil: filter option is deprecated. Use blanks: :as_nil.")
217
+ return opts[:allow_nil] ? :as_nil : :as_literal
218
+ end
219
+
220
+ blanks = opts.fetch(:blanks, filters_blanks_by_default)
221
+ unless Resource::BLANK_MODES.include?(blanks)
222
+ raise Errors::InvalidFilterBlanks.new(self, name, blanks)
223
+ end
224
+
225
+ blanks
226
+ end
227
+ private :blanks_for
209
228
  end
210
229
  end
211
230
  end
@@ -80,7 +80,7 @@ module Graphiti
80
80
  end
81
81
 
82
82
  def validate_request!(params)
83
- return if Graphiti.context[:graphql] || !validate_endpoints?
83
+ return if Graphiti.context[:graphql] || !validate_requests?
84
84
 
85
85
  if context&.respond_to?(:request)
86
86
  path = context.request.env["PATH_INFO"]
@@ -25,12 +25,8 @@ module Graphiti
25
25
  class_attribute :endpoint,
26
26
  :base_url,
27
27
  :endpoint_namespace,
28
- :secondary_endpoints,
29
- :autolink,
30
- :validate_endpoints
28
+ :secondary_endpoints
31
29
  self.secondary_endpoints = []
32
- self.autolink = true
33
- self.validate_endpoints = true
34
30
 
35
31
  class << self
36
32
  prepend Overrides
@@ -38,6 +34,33 @@ module Graphiti
38
34
  end
39
35
 
40
36
  class_methods do
37
+ # Deprecated. Folded into relationship_links. Remove in 3.0.
38
+ def autolink
39
+ relationship_links != false
40
+ end
41
+
42
+ def autolink=(val)
43
+ self.relationship_links = val ? true : false
44
+ end
45
+
46
+ def autolink?
47
+ autolink
48
+ end
49
+
50
+ # Deprecated. Split into validate_requests and validate_links. Remove in 3.0.
51
+ def validate_endpoints
52
+ validate_requests && validate_links
53
+ end
54
+
55
+ def validate_endpoints=(val)
56
+ self.validate_requests = val
57
+ self.validate_links = val
58
+ end
59
+
60
+ def validate_endpoints?
61
+ validate_endpoints
62
+ end
63
+
41
64
  def infer_endpoint
42
65
  return unless name
43
66
 
@@ -101,4 +124,12 @@ module Graphiti
101
124
  end
102
125
  end
103
126
  end
127
+
128
+ msg = "Use `self.relationship_links` (true, false, or :on_demand)"
129
+ endpoints_msg = "Use `self.validate_requests` for inbound requests, `self.validate_links` for rendered links"
130
+ DEPRECATOR.deprecate_methods(Links::ClassMethods,
131
+ autolink: msg, "autolink=": msg, autolink?: msg,
132
+ validate_endpoints: endpoints_msg,
133
+ "validate_endpoints=": endpoints_msg,
134
+ validate_endpoints?: endpoints_msg)
104
135
  end
@@ -6,7 +6,8 @@ module Graphiti
6
6
  included do
7
7
  self.adapter = Graphiti::Adapters::GraphitiAPI
8
8
  self.model = OpenStruct
9
- self.validate_endpoints = false
9
+ self.validate_requests = false
10
+ self.validate_links = false
10
11
 
11
12
  class_attribute :timeout,
12
13
  :open_timeout
@@ -126,11 +126,11 @@ module Graphiti
126
126
  end
127
127
 
128
128
  # If eager loading, ensure routes are loaded first, then apply
129
- # This happens in Railtie
129
+ # This happens in Railtie. Setup runs once, so classes redefined by a reload apply at definition.
130
130
  def eagerly_apply_sideload?(sideload)
131
131
  # TODO: Maybe handle this in the Rails integration
132
132
  if defined?(::Rails) && (app = ::Rails.application)
133
- app.config.eager_load ? false : true
133
+ app.config.eager_load ? Graphiti.setup? : true
134
134
  else
135
135
  sideload.resource_class_loaded?
136
136
  end
@@ -111,8 +111,8 @@ module Graphiti
111
111
  config[:default_sort] = default_sort
112
112
  end
113
113
 
114
- if r.default_page_size
115
- config[:default_page_size] = r.default_page_size
114
+ if r.page_default_size
115
+ config[:default_page_size] = r.page_default_size
116
116
  end
117
117
 
118
118
  if r.polymorphic? && !r.polymorphic_child?
@@ -257,15 +257,21 @@ module Graphiti
257
257
  end
258
258
  end
259
259
 
260
+ current_attributes = current_attributes_snapshot
261
+
260
262
  Concurrent::Promises.future_on(
261
- self.class.global_thread_pool_executor, Thread.current.object_id, thread_storage, fiber_storage, *args
262
- ) do |thread_id, thread_storage, fiber_storage, *args|
263
+ self.class.global_thread_pool_executor, Thread.current.object_id, thread_storage, fiber_storage, current_attributes, *args
264
+ ) do |thread_id, thread_storage, fiber_storage, current_attributes, *args|
263
265
  self.class.marking_pool_thread do
264
266
  wrap_in_rails_executor do
265
267
  with_thread_locals(thread_storage) do
266
268
  with_fiber_locals(fiber_storage) do
267
- Graphiti.broadcast(:global_thread_pool_task_run, self.class.global_thread_pool_stats) do
268
- yield(*args)
269
+ with_current_attributes(current_attributes) do
270
+ with_connection_pool_hint do
271
+ Graphiti.broadcast(:global_thread_pool_task_run, self.class.global_thread_pool_stats) do
272
+ yield(*args)
273
+ end
274
+ end
269
275
  end
270
276
  end
271
277
  end
@@ -274,6 +280,45 @@ module Graphiti
274
280
  end
275
281
  end
276
282
 
283
+ def current_attributes_snapshot
284
+ return unless defined?(ActiveSupport::CurrentAttributes)
285
+
286
+ snapshot = {}
287
+ klasses = ActiveSupport::CurrentAttributes.subclasses
288
+ while (klass = klasses.shift)
289
+ klasses.concat(klass.subclasses)
290
+ attributes = klass.attributes
291
+ snapshot[klass] = attributes.dup if attributes.any?
292
+ end
293
+ snapshot
294
+ end
295
+
296
+ # Restored inside the Rails executor, whose entry hands the pool thread a
297
+ # fresh Current. Restoring through set scopes the values to the block.
298
+ def with_current_attributes(snapshot, &block)
299
+ return yield if snapshot.nil? || snapshot.empty?
300
+
301
+ klass, attributes = snapshot.first
302
+ rest = snapshot.except(klass)
303
+ klass.set(attributes) { with_current_attributes(rest, &block) }
304
+ end
305
+
306
+ # A timeout inside a sideload task nearly always means database.yml's pool
307
+ # was not sized for the sideload threads, and the bare error does not say so.
308
+ def with_connection_pool_hint
309
+ yield
310
+ rescue => error
311
+ raise unless defined?(ActiveRecord::ConnectionTimeoutError) && error.is_a?(ActiveRecord::ConnectionTimeoutError)
312
+
313
+ hinted = error.exception(<<~MSG.strip)
314
+ #{error.message}
315
+
316
+ Raised while resolving a sideload concurrently. Each concurrent sideload holds its own database connection, so `pool` in database.yml must be at least web threads + concurrency_max_threads (#{Graphiti.config.concurrency_max_threads}) + 1. See graphiti.dev/concepts/resources#concurrency-pool-sizing.
317
+ MSG
318
+ hinted.set_backtrace(error.backtrace)
319
+ raise hinted
320
+ end
321
+
277
322
  def with_thread_locals(thread_locals)
278
323
  new_thread_locals = []
279
324
  thread_locals.each do |key, value|
@@ -62,7 +62,7 @@ module Graphiti
62
62
  value = parse_string_value(filter.values[0], value)
63
63
  end
64
64
 
65
- check_deny_empty_filters!(resource, filter, value)
65
+ check_blank_filters!(resource, filter, value)
66
66
  value = parse_string_null(filter.values[0], value)
67
67
  validate_singular(resource, filter, value)
68
68
  value = coerce_types(filter.values[0], param_name.to_sym, value)
@@ -209,15 +209,15 @@ module Graphiti
209
209
  end
210
210
 
211
211
  def parse_string_null(filter, value)
212
- return value unless filter[:allow_nil]
212
+ return value unless filter[:blanks] == :as_nil
213
213
  return value.map { |item| (item == "null") ? nil : item } if value.is_a?(Array)
214
214
  return if value == "null"
215
215
 
216
216
  value
217
217
  end
218
218
 
219
- def check_deny_empty_filters!(resource, filter, value)
220
- return unless filter.values[0][:deny_empty]
219
+ def check_blank_filters!(resource, filter, value)
220
+ return unless filter.values[0][:blanks] == :reject
221
221
 
222
222
  if value.nil? || value.empty? || value == "null"
223
223
  raise Errors::InvalidFilterValue.new(resource, filter, "(empty)")
@@ -4,9 +4,9 @@ module Graphiti
4
4
  PARAMS = [:number, :size, :offset, :before, :after]
5
5
 
6
6
  def apply
7
- if size > resource.max_page_size
7
+ if size > resource.page_max_size
8
8
  raise Graphiti::Errors::UnsupportedPageSize
9
- .new(size, resource.max_page_size)
9
+ .new(size, resource.page_max_size)
10
10
  elsif requested? && @opts[:sideload_parent_length].to_i > 1
11
11
  raise Graphiti::Errors::UnsupportedPagination
12
12
  else
@@ -100,7 +100,7 @@ module Graphiti
100
100
  end
101
101
 
102
102
  def size
103
- (page_param[:size] || resource.default_page_size || DEFAULT_PAGE_SIZE).to_i
103
+ (page_param[:size] || resource.page_default_size || DEFAULT_PAGE_SIZE).to_i
104
104
  end
105
105
  end
106
106
  end
@@ -1,5 +1,7 @@
1
1
  module Graphiti
2
2
  class Serializer < JSONAPI::Serializable::Resource
3
+ UNREQUESTED_LINKS = [false, nil, "false"].freeze
4
+
3
5
  include Graphiti::Extensions::BooleanAttribute
4
6
  include Graphiti::Extensions::ExtraAttribute
5
7
  include Graphiti::SerializableHash
@@ -59,7 +61,7 @@ module Graphiti
59
61
  starting_offset = 0
60
62
  page_param = @proxy.query.pagination
61
63
  if (page_number = page_param[:number])
62
- page_size = page_param[:size] || @resource.default_page_size
64
+ page_size = page_param[:size] || @resource.page_default_size
63
65
  starting_offset = (page_number - 1) * page_size
64
66
  end
65
67
 
@@ -100,17 +102,21 @@ module Graphiti
100
102
  hash[:links] = @resource.links(@object) if @resource.links?
101
103
  end
102
104
 
105
+ # A relationship whose only content would be an unrequested on-demand link
106
+ # serializes as an empty stub, which JSON:API forbids.
103
107
  def strip_relationships!(hash)
104
- hash[:relationships]&.select! do |name, payload|
105
- payload.key?(:data)
108
+ hash[:relationships]&.reject! do |name, payload|
109
+ next false if payload.key?(:data) || payload.key?(:links)
110
+
111
+ self.class.relationship_sideloads[name]&.link_mode == :on_demand
106
112
  end
107
113
  end
108
114
 
109
115
  def strip_relationships?
110
- return false unless Graphiti.config.links_on_demand
111
- params = Graphiti.context[:object]&.params || {}
116
+ context = Graphiti.context[:object]
117
+ params = context.params if context.respond_to?(:params)
112
118
 
113
- [false, nil, "false"].include?(params[:links])
119
+ UNREQUESTED_LINKS.include?(params && params[:links])
114
120
  end
115
121
  end
116
122
  end
@@ -31,6 +31,9 @@ module Graphiti
31
31
  @writable = opts[:writable]
32
32
  @as = opts[:as]
33
33
  @link = opts[:link]
34
+ unless @link.nil? || Resource::LINK_MODES.include?(@link)
35
+ raise Errors::InvalidLinkRendering.new(@parent_resource_class, :"#{name} link", @link)
36
+ end
34
37
  @single = opts[:single]
35
38
  @remote = opts[:remote]
36
39
  apply_belongs_to_many_filter if type == :many_to_many
@@ -84,7 +87,8 @@ module Graphiti
84
87
  self.adapter = Graphiti::Adapters::GraphitiAPI
85
88
  self.model = OpenStruct
86
89
  self.remote = remote_url
87
- self.validate_endpoints = false
90
+ self.validate_requests = false
91
+ self.validate_links = false
88
92
  }
89
93
  name = "#{parent_resource_class.name}.#{@name}.remote"
90
94
  klass.class_eval("def self.name;'#{name}';end", __FILE__, __LINE__)
@@ -166,14 +170,16 @@ module Graphiti
166
170
  false
167
171
  end
168
172
 
169
- def link?
170
- return true if link_proc
173
+ # A custom link block means the author wants the link, so a false default does not silence it.
174
+ def link_mode
175
+ return @link unless @link.nil?
171
176
 
172
- if @link.nil?
173
- !!@parent_resource_class.autolink
174
- else
175
- !!@link
176
- end
177
+ default = @parent_resource_class.relationship_links
178
+ (link_proc && default == false) ? true : default
179
+ end
180
+
181
+ def link?
182
+ link_mode != false
177
183
  end
178
184
 
179
185
  def link_filter(parents)
@@ -140,7 +140,7 @@ module Graphiti
140
140
  end
141
141
 
142
142
  class FilterAttributeMatcher < ResourceDSLMatcher
143
- GRAPHITI_OPTS = %i[allow deny single required allow_nil deny_empty].freeze
143
+ GRAPHITI_OPTS = %i[allow deny single required blanks].freeze
144
144
  GRAPHITI_CONFIG_KEY = :filters
145
145
  EXPECTED_ACTION = "filter"
146
146
  end
@@ -186,7 +186,7 @@ module Graphiti
186
186
  # @param [Symbol] type
187
187
  #
188
188
  # @example expect(subject).to filter_attribute(:name, :string)
189
- # @example expect(subject).to filter_attribute(:name, :string).with_options(allow_nil: false)
189
+ # @example expect(subject).to filter_attribute(:name, :string).with_options(blanks: :as_nil)
190
190
  # @example expect(subject).not_to filter_attribute(:name, :string)
191
191
  def filter_attribute(attribute, type)
192
192
  FilterAttributeMatcher.new(attribute, type)
@@ -39,14 +39,17 @@ resource_testing = proc do
39
39
  let(:params) { {} }
40
40
 
41
41
  around do |e|
42
- original = Graphiti::Resource.validate_endpoints
43
- Graphiti::Resource.validate_endpoints = false
42
+ original_requests = Graphiti::Resource.validate_requests
43
+ original_links = Graphiti::Resource.validate_links
44
+ Graphiti::Resource.validate_requests = false
45
+ Graphiti::Resource.validate_links = false
44
46
 
45
47
  Graphiti.with_context graphiti_context do
46
48
  e.run
47
49
  end
48
50
  ensure
49
- Graphiti::Resource.validate_endpoints = original
51
+ Graphiti::Resource.validate_requests = original_requests
52
+ Graphiti::Resource.validate_links = original_links
50
53
  end
51
54
 
52
55
  def graphiti_context
@@ -47,7 +47,7 @@ module Graphiti
47
47
  end
48
48
 
49
49
  def on_demand_links(url)
50
- return url unless Graphiti.config.links_on_demand
50
+ return url unless @sideload.resource.relationship_links == :on_demand
51
51
  return unless url
52
52
 
53
53
  url << if url.include?("?")
@@ -30,7 +30,7 @@ module Graphiti
30
30
  @serializer.send(:"#{applied_method}=", [@name] | existing)
31
31
 
32
32
  @serializer.meta do
33
- if !!@resource.try(:cursor_paginatable?) && !Graphiti.context[:graphql]
33
+ if !!@resource.try(:page_cursors?) && !Graphiti.context[:graphql]
34
34
  {cursor: cursor}
35
35
  end
36
36
  end
@@ -121,10 +121,11 @@ module Graphiti
121
121
 
122
122
  def default_proc
123
123
  name_ref = @name
124
+ resource_ref = @resource
124
125
  typecast_ref = typecast(Graphiti::Types[@attr[:type]][:read])
125
126
  ->(_) {
126
127
  val = @object.send(name_ref)
127
- if Graphiti.config.typecast_reads
128
+ if resource_ref.typecast_reads
128
129
  typecast_ref.call(val)
129
130
  else
130
131
  val
@@ -133,10 +134,11 @@ module Graphiti
133
134
  end
134
135
 
135
136
  def wrap_proc(inner)
137
+ resource_ref = @resource
136
138
  typecast_ref = typecast(Graphiti::Types[@attr[:type]][:read])
137
139
  ->(serializer_instance = nil) {
138
140
  val = serializer_instance.instance_eval(&inner)
139
- if Graphiti.config.typecast_reads
141
+ if resource_ref.typecast_reads
140
142
  typecast_ref.call(val)
141
143
  else
142
144
  val
@@ -8,6 +8,8 @@ module Graphiti
8
8
  end
9
9
 
10
10
  def apply
11
+ return unless @serializer
12
+
11
13
  @sideloads.each_pair do |name, sideload|
12
14
  if apply?(sideload)
13
15
  SerializerRelationship
@@ -55,7 +57,6 @@ module Graphiti
55
57
  private
56
58
 
57
59
  def block
58
- link_ref = link?
59
60
  sideload_ref = @sideload
60
61
  resource_class_ref = @resource_class
61
62
  data_proc_ref = data_proc
@@ -93,13 +94,11 @@ module Graphiti
93
94
  linkage always: sideload_ref.render_resource_ids?
94
95
  end
95
96
 
96
- if link_ref
97
- if @proxy.query.links?
98
- self_ref.send(:validate_link!) unless self_ref.send(:eagerly_validate_links?)
97
+ if @proxy.query.render_link?(sideload_ref.link_mode) && self_ref.send(:linkable?)
98
+ self_ref.send(:validate_link!) unless self_ref.send(:eagerly_validate_links?)
99
99
 
100
- link(:related) do
101
- ::Graphiti::Util::Link.new(sideload_ref, @object).generate
102
- end
100
+ link(:related) do
101
+ ::Graphiti::Util::Link.new(sideload_ref, @object).generate
103
102
  end
104
103
  end
105
104
  end
@@ -162,8 +161,8 @@ module Graphiti
162
161
  end
163
162
 
164
163
  def validate_link!
165
- return unless link?
166
- return unless @resource_class.validate_endpoints?
164
+ return unless @sideload.link? && linkable?
165
+ return unless @resource_class.validate_links?
167
166
  return if @sideload.link_proc
168
167
 
169
168
  unless Graphiti.config.context_for_endpoint
@@ -192,14 +191,16 @@ module Graphiti
192
191
  self.class.validated_link_cache << cache_key
193
192
  end
194
193
 
195
- def link?
196
- return true if @sideload.link_proc
194
+ # Checked lazily so a sideload with no endpoint only raises when a link is actually wanted.
195
+ def linkable?
196
+ return @linkable if defined?(@linkable)
197
197
 
198
- if @sideload.respond_to?(:children)
199
- @sideload.link? &&
200
- @sideload.children.values.all? { |c| !c.resource.endpoint.nil? }
198
+ @linkable = if @sideload.link_proc
199
+ true
200
+ elsif @sideload.respond_to?(:children)
201
+ @sideload.children.values.all? { |c| !c.resource.endpoint.nil? }
201
202
  else
202
- !!(@sideload.link? && @sideload.resource.endpoint)
203
+ !@sideload.resource.endpoint.nil?
203
204
  end
204
205
  end
205
206
  end
@@ -1,3 +1,3 @@
1
1
  module Graphiti
2
- VERSION = "2.0.0.beta.9"
2
+ VERSION = "2.0.0.beta.10"
3
3
  end
data/lib/graphiti.rb CHANGED
@@ -149,6 +149,11 @@ module Graphiti
149
149
  resources.each do |r|
150
150
  r.apply_sideloads_to_serializer
151
151
  end
152
+ @setup = true
153
+ end
154
+
155
+ def self.setup?
156
+ !!@setup
152
157
  end
153
158
 
154
159
  def self.cache=(val)
@@ -22,6 +22,10 @@ namespace :graphiti do
22
22
  rows = Graphiti::Audit.run
23
23
  puts Graphiti::Audit::Report.new(rows)
24
24
 
25
+ if (advisory = helpers.connection_pool_advisory)
26
+ puts advisory
27
+ end
28
+
25
29
  exit 1 if rows.any?(&:error?)
26
30
  end
27
31
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphiti
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0.beta.9
4
+ version: 2.0.0.beta.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lee Richmond