graphiti 2.0.0.beta.8 → 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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +1 -1
  3. data/.github/workflows/docs.yml +9 -6
  4. data/.github/workflows/release.yml +1 -1
  5. data/CHANGELOG.md +43 -0
  6. data/Rakefile +28 -0
  7. data/UPGRADING.md +1 -1
  8. data/lib/generators/graphiti/generator_mixin.rb +24 -0
  9. data/lib/generators/graphiti/templates/application_resource.rb.erb +9 -16
  10. data/lib/graphiti/configuration.rb +66 -28
  11. data/lib/graphiti/debugger.rb +18 -6
  12. data/lib/graphiti/delegates/pagination.rb +2 -2
  13. data/lib/graphiti/error_serializers/deprecated_constants.rb +23 -0
  14. data/lib/graphiti/error_serializers/invalid_request.rb +6 -0
  15. data/lib/graphiti/errors.rb +51 -3
  16. data/lib/graphiti/query.rb +36 -14
  17. data/lib/graphiti/rails/controller.rb +26 -6
  18. data/lib/graphiti/rails/rake_helpers.rb +17 -0
  19. data/lib/graphiti/rails.rb +14 -0
  20. data/lib/graphiti/railtie.rb +8 -0
  21. data/lib/graphiti/request_validators/update_validator.rb +1 -1
  22. data/lib/graphiti/request_validators/validator.rb +9 -0
  23. data/lib/graphiti/resource/configuration.rb +138 -40
  24. data/lib/graphiti/resource/dsl.rb +21 -2
  25. data/lib/graphiti/resource/interface.rb +1 -1
  26. data/lib/graphiti/resource/links.rb +36 -5
  27. data/lib/graphiti/resource/remote.rb +2 -1
  28. data/lib/graphiti/resource/sideloading.rb +2 -2
  29. data/lib/graphiti/schema.rb +2 -2
  30. data/lib/graphiti/scope.rb +115 -23
  31. data/lib/graphiti/scoping/filter.rb +4 -4
  32. data/lib/graphiti/scoping/paginate.rb +3 -3
  33. data/lib/graphiti/serializer.rb +12 -6
  34. data/lib/graphiti/sideload/polymorphic_belongs_to.rb +11 -4
  35. data/lib/graphiti/sideload.rb +22 -14
  36. data/lib/graphiti/spec_helpers/matchers.rb +2 -2
  37. data/lib/graphiti/spec_helpers/rspec.rb +6 -3
  38. data/lib/graphiti/util/link.rb +1 -1
  39. data/lib/graphiti/util/serializer_attributes.rb +5 -3
  40. data/lib/graphiti/util/serializer_relationships.rb +26 -16
  41. data/lib/graphiti/version.rb +1 -1
  42. data/lib/graphiti.rb +6 -1
  43. data/lib/tasks/graphiti.rake +4 -0
  44. data/package-lock.json +114 -0
  45. data/package.json +16 -3
  46. metadata +3 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b5605ecf7bc316cef1342565d7cd54cf7136a98915d669905550dcce32452d7c
4
- data.tar.gz: 71be03dd2e18fed5485fe154957e8d5366ec21d648cfaf023281277e7d7c5e5a
3
+ metadata.gz: b1e331ab2dea47987ea47260eb0890c9a953d3435bba2e7ef37cbfbbc83fe5ab
4
+ data.tar.gz: 13a7483ead9f4ad5ed69390ff6a4a122a819813c2147c0a56dceebdfebc57e17
5
5
  SHA512:
6
- metadata.gz: facebd92bdb5be565324696a089595e2b224cbf79872e60cc92dc4422ded817ad22937a3e9b9008744140f55b1519b97f00cb0e7708a92a7bfda88d5339a18a9
7
- data.tar.gz: cb80adbf97d38e470c1a6b56bb0cbe49b59abfbe4a81cc0438696cc6452b2677a6f4d2d43a859c6eb4e06815ac4912ae3aa4c52085fbff89f8f05b1f5c9c32eb
6
+ metadata.gz: cca252389b52b5a2983407b4eff7cf7fe8b49759b478228f9d78303522bc40f2948a1784d6c863373f10521dc80b61d35f201009e830d8a968cc989b37d2ad81
7
+ data.tar.gz: 3fdfda722d09ddea6c366ca4102258bdea6d36c923be729ae141063e7e62b43ae0730f30a6422ae4448f858a5c1d4269f80180279d7939ce050c88008613cedb
@@ -139,7 +139,7 @@ jobs:
139
139
  # Only main auto-releases. Beta releases are cut by manually running
140
140
  # "Generate New Release" with the beta branch selected.
141
141
  if: github.ref == 'refs/heads/main'
142
- needs: [test]
142
+ needs: [lint, test]
143
143
  steps:
144
144
  - name: Dispatch Release
145
145
  uses: benc-uk/workflow-dispatch@v1
@@ -1,20 +1,23 @@
1
1
  name: Docs
2
2
 
3
- # Publishes the documentation site from beta. graphiti.dev now points at this
4
- # repo via website/static/CNAME, so this deploy is the live site: the 1.x
5
- # Jekyll site is frozen under website/static/1.13 and ships with every build,
6
- # and graphiti-api.github.io no longer serves the domain.
3
+ # Publishes the documentation site from beta, and from main once 2.0 lands
4
+ # there. graphiti.dev now points at this repo via website/static/CNAME, so this
5
+ # deploy is the live site: the 1.x Jekyll site is frozen under
6
+ # website/static/1.13 and ships with every build, and graphiti-api.github.io no
7
+ # longer serves the domain.
7
8
  on:
8
9
  push:
9
- branches: [beta]
10
+ branches: [beta, main]
10
11
  paths:
11
12
  - 'docs/**'
12
13
  - 'website/**'
13
14
  - '.github/workflows/docs.yml'
14
15
  workflow_dispatch: {}
15
16
 
17
+ # One static group: deploys from different branches force-push the same
18
+ # gh-pages branch, so they must not interleave.
16
19
  concurrency:
17
- group: docs-${{ github.ref }}
20
+ group: docs
18
21
  cancel-in-progress: true
19
22
 
20
23
  permissions:
@@ -16,7 +16,7 @@ jobs:
16
16
  - name: Set up Ruby
17
17
  uses: ruby/setup-ruby@v1
18
18
  with:
19
- ruby-version: "3.4"
19
+ ruby-version: "4.0"
20
20
  - name: Build
21
21
  run: |
22
22
  gem install bundler
data/CHANGELOG.md CHANGED
@@ -1,5 +1,48 @@
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
+
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)
26
+
27
+
28
+ ### Bug Fixes
29
+
30
+ * bridge the last 1.x names that died with a bare NameError ([34e1dbd](https://github.com/graphiti-api/graphiti/commit/34e1dbd3e34bb8e6be56292c51fcb32674f75995))
31
+ * build the entity map when the root query is, not on first use ([134d18b](https://github.com/graphiti-api/graphiti/commit/134d18b12b9ebb6903cca813209f60e5626b25a6))
32
+ * coerce debug flags from env strings ([46be6aa](https://github.com/graphiti-api/graphiti/commit/46be6aa05d4e2a9e5d6b2562f223b4b05582bb39))
33
+ * keep a customized sideload's instances out of entity dedup ([b1beee3](https://github.com/graphiti-api/graphiti/commit/b1beee3288fae2b20b009983aaa2f6c71189e2ca))
34
+ * raise a more helpful named error when belongs_to linkage reads an unselected foreign key ([f59ea0b](https://github.com/graphiti-api/graphiti/commit/f59ea0bda94b388b1c2c0ae0e50b258f8ce0eb11))
35
+ * resolve inline when already on a sideload pool thread ([11c9de7](https://github.com/graphiti-api/graphiti/commit/11c9de7fb4bfc2d83b02aaba3e58c1f94c84cec4))
36
+ * scope debugger chunks to the request rather than the class ([9a77896](https://github.com/graphiti-api/graphiti/commit/9a77896c6e56d23b89d0a2aef358d8e8e0c28c54))
37
+ * surface the first child error from concurrent polymorphic sideloads ([5a08c0e](https://github.com/graphiti-api/graphiti/commit/5a08c0edab2bc1ee0c87c6abbe1067297569411e))
38
+
39
+
40
+ ### Performance Improvements
41
+
42
+ * nest the entity map instead of keying it by an array ([20bf625](https://github.com/graphiti-api/graphiti/commit/20bf6259d0347b02568b7d47054bb799e69629c3))
43
+ * skip the promise machinery when nothing is sideloaded ([d879923](https://github.com/graphiti-api/graphiti/commit/d879923070a35d1a0e908b2d04746aa81effb262))
44
+ * skip zipping a lone sideload promise ([7ef2cea](https://github.com/graphiti-api/graphiti/commit/7ef2cea654c48175504105f907b5b651c9fcdaf3))
45
+
3
46
  # [2.0.0-beta.8](https://github.com/graphiti-api/graphiti/compare/v2.0.0-beta.7...v2.0.0-beta.8) (2026-08-10)
4
47
 
5
48
 
data/Rakefile CHANGED
@@ -11,6 +11,34 @@ RSpec::Core::RakeTask.new(:spec) do |t|
11
11
  end
12
12
  end
13
13
 
14
+ namespace :performance do
15
+ # Shelling out keeps a failure to the script's own message, with no rake backtrace on top.
16
+ def measure(*arguments)
17
+ exit(1) unless system("bundle", "exec", "ruby", "spec/performance/measure_releases.rb", *arguments)
18
+ end
19
+
20
+ desc "Compare the working tree to the last release, and plot it"
21
+ task :current do
22
+ measure
23
+ end
24
+
25
+ desc "Plot the release history plus the working tree to tmp/performance.html and open it"
26
+ task :page do
27
+ exit(1) unless system("bundle", "exec", "ruby", "spec/performance/chart_page.rb")
28
+ end
29
+
30
+ desc "Measure one release and record it (TAG=v2.0.0-beta.9)"
31
+ task :record do
32
+ tag = ENV["TAG"] or abort "pass the release to record, e.g. rake performance:record TAG=v2.0.0-beta.9"
33
+ measure(tag)
34
+ end
35
+
36
+ desc "Re-record every release for this ruby, replacing its rows"
37
+ task :record_all do
38
+ measure("--all")
39
+ end
40
+ end
41
+
14
42
  if ENV["APPRAISAL_INITIALIZED"]
15
43
  task default: [:spec]
16
44
  else
data/UPGRADING.md CHANGED
@@ -2,4 +2,4 @@
2
2
 
3
3
  The upgrade guide lives with the rest of the documentation, so it can link into the concept pages and offer the same version picker: **https://graphiti.dev/upgrading**
4
4
 
5
- In this repository it is [docs/upgrading/](docs/upgrading/index.md).
5
+ In this repository it is [docs/upgrading.md](docs/upgrading.md).
@@ -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?
@@ -87,13 +63,20 @@ module Graphiti
87
63
  end
88
64
 
89
65
  def debug=(val)
90
- @debug = val
91
- Debugger.enabled = val
66
+ @debug = coerce_flag(val)
67
+ Debugger.enabled = @debug
92
68
  end
93
69
 
94
70
  def debug_models=(val)
95
- @debug_models = val
96
- Debugger.debug_models = val
71
+ @debug_models = coerce_flag(val)
72
+ Debugger.debug_models = @debug_models
73
+ end
74
+
75
+ # every value comes back from ENV as a string and "false" is truthy
76
+ def coerce_flag(val)
77
+ return false if ["false", "0", ""].include?(val.to_s.strip.downcase)
78
+
79
+ !!val
97
80
  end
98
81
 
99
82
  def with_option(key, value)
@@ -104,6 +87,48 @@ module Graphiti
104
87
  send(:"#{key}=", original)
105
88
  end
106
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
+
107
132
  def uri_decoder=(decoder)
108
133
  unless decoder.respond_to?(:call)
109
134
  raise "uri_decoder must respond to `call`."
@@ -134,4 +159,17 @@ module Graphiti
134
159
 
135
160
  msg = "Use `config.graphiti.respond_to_formats`"
136
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)
137
175
  end
@@ -4,11 +4,22 @@
4
4
  module Graphiti
5
5
  class Debugger
6
6
  class << self
7
- attr_accessor :enabled, :chunks, :debug_models, :preserve, :pry
7
+ attr_accessor :enabled, :debug_models, :preserve, :pry
8
8
  end
9
- self.chunks = []
9
+
10
+ CHUNKS = :__graphiti_debugger_chunks
11
+ private_constant :CHUNKS
10
12
 
11
13
  class << self
14
+ # Pool threads inherit this through the thread-local copy Scope#future_with_context makes, so their chunks reach the right request.
15
+ def chunks
16
+ Thread.current[CHUNKS] ||= Concurrent::Array.new
17
+ end
18
+
19
+ def chunks=(value)
20
+ Thread.current[CHUNKS] = value
21
+ end
22
+
12
23
  def on_data(name, start, stop, id, payload)
13
24
  return [] unless enabled
14
25
 
@@ -179,14 +190,15 @@ module Graphiti
179
190
  end
180
191
 
181
192
  def graph_statements
182
- @chunks.each do |chunk|
193
+ statements = chunks
194
+ statements.each do |chunk|
183
195
  if (parent = chunk[:parent])
184
- relevant = chunks.find { |c| c[:resource] == parent }
196
+ relevant = statements.find { |c| c[:resource] == parent }
185
197
  relevant[:children].unshift(chunk) if relevant
186
198
  end
187
199
  end
188
- @chunks.reject! { |c| !!c[:parent] }
189
- @chunks
200
+ statements.reject! { |c| !!c[:parent] }
201
+ statements
190
202
  end
191
203
 
192
204
  def chunk_to_hash(chunk)
@@ -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
 
@@ -23,6 +23,29 @@ module GraphitiErrors
23
23
  MSG
24
24
  end
25
25
 
26
+ # The old global rendering toggle, most often flipped in specs.
27
+ class << self
28
+ def enable!
29
+ Graphiti::DEPRECATOR.deprecation_warning("GraphitiErrors.enable!", "wrap the request in Graphiti::Rails::TestHelpers#handle_request_exceptions instead")
30
+ test_helpers.handle_request_exceptions(true)
31
+ end
32
+
33
+ def disable!
34
+ Graphiti::DEPRECATOR.deprecation_warning("GraphitiErrors.disable!", "wrap the request in Graphiti::Rails::TestHelpers#handle_request_exceptions instead")
35
+ test_helpers.handle_request_exceptions(false)
36
+ end
37
+
38
+ def disabled?
39
+ !test_helpers.handle_request_exceptions?
40
+ end
41
+
42
+ private
43
+
44
+ def test_helpers
45
+ @test_helpers ||= Object.new.extend(Graphiti::Rails::TestHelpers)
46
+ end
47
+ end
48
+
26
49
  module Validation; end
27
50
 
28
51
  module InvalidRequest; end
@@ -36,6 +36,12 @@ module Graphiti
36
36
  end
37
37
  end
38
38
 
39
+ # graphiti_errors named the payload this. Remove in 3.0.
40
+ def rendered_errors
41
+ Graphiti::DEPRECATOR.deprecation_warning("#rendered_errors", "use #errors")
42
+ errors
43
+ end
44
+
39
45
  private
40
46
 
41
47
  def code
@@ -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,52 @@ 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
+
743
+ class UnselectedForeignKey < Base
744
+ def initialize(resource_class, sideload, model)
745
+ @resource_class = resource_class
746
+ @sideload = sideload
747
+ @model = model
748
+ end
749
+
750
+ def message
751
+ <<~MSG
752
+ #{@resource_class.name}: rendering resource linkage for relationship #{@sideload.name.inspect} reads ##{@sideload.foreign_key} off #{@model.class.name}, but that attribute is not loaded.
753
+
754
+ Keep #{@sideload.foreign_key} in the selected columns, or turn linkage off with `resource_ids: false` on the relationship or `self.belongs_to_resource_ids_by_default = :never` on the resource.
755
+ MSG
756
+ end
757
+ end
758
+
711
759
  class MissingRelationshipMethod < Base
712
760
  def initialize(resource_class, sideload, model)
713
761
  @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)
@@ -22,6 +25,7 @@ module Graphiti
22
25
  @include_param = nested_include || @params[:include]
23
26
  @parents = parents
24
27
  @action = parse_action(action)
28
+ @entity_map = Concurrent::Map.new if parents.empty?
25
29
  end
26
30
 
27
31
  def association?
@@ -32,32 +36,46 @@ module Graphiti
32
36
  def entity_map
33
37
  return root.entity_map unless root == self
34
38
 
35
- @entity_map ||= Concurrent::Map.new
39
+ @entity_map
36
40
  end
37
41
 
38
42
  def top_level?
39
43
  !association?
40
44
  end
41
45
 
42
- def links?
43
- return false if [:json, :xml, "json", "xml"].include?(params[:format])
44
- if Graphiti.config.links_on_demand
45
- [true, "true"].include?(@params[:links])
46
- else
47
- true
48
- 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])
49
56
  end
50
57
 
51
- 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?
52
65
  if action == :find
53
66
  false
54
- elsif Graphiti.config.pagination_links_on_demand
55
- [true, "true"].include?(@params[:pagination_links])
67
+ elsif @resource.page_links == :on_demand
68
+ page_links_requested?
56
69
  else
57
- Graphiti.config.pagination_links
70
+ @resource.page_links
58
71
  end
59
72
  end
60
73
 
74
+ def page_links_requested?
75
+ [@params[:page_links], @params[:pagination_links]]
76
+ .any? { |value| [true, "true"].include?(value) }
77
+ end
78
+
61
79
  def debug_requested?
62
80
  !!@params[:debug]
63
81
  end
@@ -266,8 +284,8 @@ module Graphiti
266
284
  def query_cache_key
267
285
  attrs = {extra_fields: extra_fields,
268
286
  fields: fields,
269
- links: links?,
270
- pagination_links: pagination_links?,
287
+ links: suppress_links? ? :none : links_requested?,
288
+ page_links: page_links?,
271
289
  format: params[:format]}
272
290
 
273
291
  Digest::SHA1.hexdigest(attrs.to_s)
@@ -386,5 +404,9 @@ module Graphiti
386
404
  action
387
405
  end
388
406
  end
407
+
408
+ alias_method :pagination_links?, :page_links?
409
+ DEPRECATOR.deprecate_methods(self,
410
+ pagination_links?: "Use `page_links?`")
389
411
  end
390
412
  end