graphiti 2.0.0.beta.8 → 2.0.0.beta.9

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: b5605ecf7bc316cef1342565d7cd54cf7136a98915d669905550dcce32452d7c
4
- data.tar.gz: 71be03dd2e18fed5485fe154957e8d5366ec21d648cfaf023281277e7d7c5e5a
3
+ metadata.gz: a61d00c6968d2dc24afb8986d7b3b6051394ea73ac5d5a8104f1296f650fef47
4
+ data.tar.gz: bf367c2e3ff33e4240a3f11e3fbae780c75b7f727863e61fc0c546ae9cdfb5fe
5
5
  SHA512:
6
- metadata.gz: facebd92bdb5be565324696a089595e2b224cbf79872e60cc92dc4422ded817ad22937a3e9b9008744140f55b1519b97f00cb0e7708a92a7bfda88d5339a18a9
7
- data.tar.gz: cb80adbf97d38e470c1a6b56bb0cbe49b59abfbe4a81cc0438696cc6452b2677a6f4d2d43a859c6eb4e06815ac4912ae3aa4c52085fbff89f8f05b1f5c9c32eb
6
+ metadata.gz: 4bb22545b0a2b590fb07a237857407a5a50ac2423dd33a5621af164d93b4d36237f54339fb083895e6d349ae64da3cf9cc2e24c363f63478cea691622f130331
7
+ data.tar.gz: 9da999b97fa15b04ca97420d6a6942c5d32b70bd0e8f27e33e01b8861cdd0206342d6afd11db15eb0c1d445beda4ce1f6a5ff5cceffcb54d5917e3a185cdc16a
@@ -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,26 @@
1
1
  graphiti changelog
2
2
 
3
+ # [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
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * bridge the last 1.x names that died with a bare NameError ([34e1dbd](https://github.com/graphiti-api/graphiti/commit/34e1dbd3e34bb8e6be56292c51fcb32674f75995))
9
+ * build the entity map when the root query is, not on first use ([134d18b](https://github.com/graphiti-api/graphiti/commit/134d18b12b9ebb6903cca813209f60e5626b25a6))
10
+ * coerce debug flags from env strings ([46be6aa](https://github.com/graphiti-api/graphiti/commit/46be6aa05d4e2a9e5d6b2562f223b4b05582bb39))
11
+ * keep a customized sideload's instances out of entity dedup ([b1beee3](https://github.com/graphiti-api/graphiti/commit/b1beee3288fae2b20b009983aaa2f6c71189e2ca))
12
+ * raise a more helpful named error when belongs_to linkage reads an unselected foreign key ([f59ea0b](https://github.com/graphiti-api/graphiti/commit/f59ea0bda94b388b1c2c0ae0e50b258f8ce0eb11))
13
+ * resolve inline when already on a sideload pool thread ([11c9de7](https://github.com/graphiti-api/graphiti/commit/11c9de7fb4bfc2d83b02aaba3e58c1f94c84cec4))
14
+ * scope debugger chunks to the request rather than the class ([9a77896](https://github.com/graphiti-api/graphiti/commit/9a77896c6e56d23b89d0a2aef358d8e8e0c28c54))
15
+ * surface the first child error from concurrent polymorphic sideloads ([5a08c0e](https://github.com/graphiti-api/graphiti/commit/5a08c0edab2bc1ee0c87c6abbe1067297569411e))
16
+
17
+
18
+ ### Performance Improvements
19
+
20
+ * nest the entity map instead of keying it by an array ([20bf625](https://github.com/graphiti-api/graphiti/commit/20bf6259d0347b02568b7d47054bb799e69629c3))
21
+ * skip the promise machinery when nothing is sideloaded ([d879923](https://github.com/graphiti-api/graphiti/commit/d879923070a35d1a0e908b2d04746aa81effb262))
22
+ * skip zipping a lone sideload promise ([7ef2cea](https://github.com/graphiti-api/graphiti/commit/7ef2cea654c48175504105f907b5b651c9fcdaf3))
23
+
3
24
  # [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
25
 
5
26
 
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).
@@ -87,13 +87,20 @@ module Graphiti
87
87
  end
88
88
 
89
89
  def debug=(val)
90
- @debug = val
91
- Debugger.enabled = val
90
+ @debug = coerce_flag(val)
91
+ Debugger.enabled = @debug
92
92
  end
93
93
 
94
94
  def debug_models=(val)
95
- @debug_models = val
96
- Debugger.debug_models = val
95
+ @debug_models = coerce_flag(val)
96
+ Debugger.debug_models = @debug_models
97
+ end
98
+
99
+ # every value comes back from ENV as a string and "false" is truthy
100
+ def coerce_flag(val)
101
+ return false if ["false", "0", ""].include?(val.to_s.strip.downcase)
102
+
103
+ !!val
97
104
  end
98
105
 
99
106
  def with_option(key, value)
@@ -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)
@@ -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
@@ -708,6 +708,22 @@ module Graphiti
708
708
  end
709
709
  end
710
710
 
711
+ class UnselectedForeignKey < Base
712
+ def initialize(resource_class, sideload, model)
713
+ @resource_class = resource_class
714
+ @sideload = sideload
715
+ @model = model
716
+ end
717
+
718
+ def message
719
+ <<~MSG
720
+ #{@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.
721
+
722
+ 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.
723
+ MSG
724
+ end
725
+ end
726
+
711
727
  class MissingRelationshipMethod < Base
712
728
  def initialize(resource_class, sideload, model)
713
729
  @resource_class = resource_class
@@ -22,6 +22,7 @@ module Graphiti
22
22
  @include_param = nested_include || @params[:include]
23
23
  @parents = parents
24
24
  @action = parse_action(action)
25
+ @entity_map = Concurrent::Map.new if parents.empty?
25
26
  end
26
27
 
27
28
  def association?
@@ -32,7 +33,7 @@ module Graphiti
32
33
  def entity_map
33
34
  return root.entity_map unless root == self
34
35
 
35
- @entity_map ||= Concurrent::Map.new
36
+ @entity_map
36
37
  end
37
38
 
38
39
  def top_level?
@@ -31,6 +31,13 @@ module Graphiti
31
31
  autoload :Responders, "graphiti/rails/responders"
32
32
  autoload :TestHelpers, "graphiti/rails/test_helpers"
33
33
 
34
+ # Deprecated. Backed graphiti-rails' enable!/disable! spec toggles. Remove in 3.0.
35
+ GraphitiErrorsTesting = ActiveSupport::Deprecation::DeprecatedConstantProxy.new(
36
+ "Graphiti::Rails::GraphitiErrorsTesting",
37
+ "Graphiti::Rails::TestHelpers",
38
+ DEPRECATOR
39
+ )
40
+
34
41
  # @!attribute self.handled_exception_formats
35
42
  # A list of formats as symbols whose exceptions will be handled by Graphiti. See {Railtie}.
36
43
  cattr_accessor :handled_exception_formats, default: []
@@ -47,6 +54,13 @@ module Graphiti
47
54
  "Graphiti::Rails::Responders",
48
55
  DEPRECATOR
49
56
  )
57
+
58
+ # Deprecated. Core's railtie merged into graphiti-rails'. Remove in 3.0.
59
+ Railtie = ActiveSupport::Deprecation::DeprecatedConstantProxy.new(
60
+ "Graphiti::Railtie",
61
+ "Graphiti::Rails::Railtie",
62
+ DEPRECATOR
63
+ )
50
64
  end
51
65
 
52
66
  ActiveSupport.on_load(:active_record) do
@@ -0,0 +1,8 @@
1
+ # Deprecated require path, kept so a leftover 1.x require does not raise.
2
+ # The railtie lives in Graphiti::Rails as of 2.0. Remove in 3.0.
3
+ require "graphiti"
4
+
5
+ Graphiti::DEPRECATOR.warn(
6
+ 'require "graphiti/railtie" is no longer needed. The railtie is part of the Rails ' \
7
+ "integration as of 2.0, and loads with graphiti when Rails is present."
8
+ )
@@ -31,6 +31,28 @@ module Graphiti
31
31
  end
32
32
  end
33
33
 
34
+ POOL_THREAD = :__graphiti_pool_thread
35
+ private_constant :POOL_THREAD
36
+
37
+ # A pool thread that waits on the pool deadlocks, since the task it waits for cannot start until the waiting thread frees its slot.
38
+ def self.resolve_synchronously?
39
+ !Graphiti.config.concurrency || on_pool_thread?
40
+ end
41
+
42
+ # TODO: move to Fiber[] once the floor is Ruby 3.2
43
+ def self.on_pool_thread?
44
+ Thread.current[POOL_THREAD] == true
45
+ end
46
+
47
+ # Restores rather than clears because :caller_runs may have run the task on a request thread.
48
+ def self.marking_pool_thread
49
+ previous = Thread.current[POOL_THREAD]
50
+ Thread.current[POOL_THREAD] = true
51
+ yield
52
+ ensure
53
+ Thread.current[POOL_THREAD] = previous
54
+ end
55
+
34
56
  def initialize(object, resource, query, opts = {})
35
57
  @object = object
36
58
  @resource = resource
@@ -43,24 +65,24 @@ module Graphiti
43
65
  end
44
66
 
45
67
  def resolve(&blk)
46
- # When concurrency is disabled, take a synchronous path that mirrors the
47
- # pre-1.8 semantics. This avoids allocating Concurrent::Promises futures,
48
- # Thread/Fiber storage snapshots, and Rails executor wrappers on every
49
- # request purely to drive a thread pool that is intentionally synchronous.
68
+ # The caller blocks on .value! either way, so concurrency only benefits parallel sideloads
50
69
  # See https://github.com/graphiti-api/graphiti/issues/505
51
- if Graphiti.config.concurrency
52
- future_resolve(&blk).value!
53
- else
70
+ if self.class.resolve_synchronously? || !applicable_sideloads?
54
71
  sync_resolve(&blk)
72
+ else
73
+ future_resolve(&blk).value!
55
74
  end
56
75
  end
57
76
 
58
77
  def resolve_sideloads(results)
59
- if Graphiti.config.concurrency
60
- future_resolve_sideloads(results).value!
61
- else
78
+ if self.class.resolve_synchronously?
62
79
  sync_resolve_sideloads(results)
80
+ else
81
+ future_resolve_sideloads(results).value!
63
82
  end
83
+
84
+ # Never return the sideloads hash, a caller mutating it would mess up the cache key
85
+ nil
64
86
  end
65
87
 
66
88
  def future_resolve(&blk)
@@ -138,10 +160,6 @@ module Graphiti
138
160
  Graphiti.config.before_sideload&.call(Graphiti.context)
139
161
  sideload.resolve(results, sideload_query, @resource)
140
162
  end
141
-
142
- # Match pre-1.8 semantics: the non-concurrent resolve_sideloads returned
143
- # nil (not the sideloads Hash). Callers don't rely on the return value.
144
- nil
145
163
  end
146
164
 
147
165
  # Resolve this scope's own data: run hooks, resolve the resource, and
@@ -165,14 +183,36 @@ module Graphiti
165
183
  # canonical instance. The resource class in the key keeps two resources
166
184
  # serving the same model from sharing an instance and a serializer.
167
185
  def deduplicate_entities!(resolved)
186
+ return unless deduplicable?
187
+
188
+ # Nested maps are built once, a composite key meant a new array for every record
189
+ by_model = @query.entity_map.compute_if_absent(@resource.class) { Concurrent::Map.new }
190
+
168
191
  resolved.map! do |record|
169
192
  next record unless record.respond_to?(:id) && !record.id.nil?
170
193
 
171
- key = [@resource.class, record.class, record.id]
172
- @query.entity_map.compute_if_absent(key) { record }
194
+ by_id = by_model.compute_if_absent(record.class) { Concurrent::Map.new }
195
+ by_id.compute_if_absent(record.id) { record }
173
196
  end
174
197
  end
175
198
 
199
+ # A customized sideload can load a record another path would not, so it keeps its own instances.
200
+ def deduplicable?
201
+ sideload = @opts[:sideload]
202
+ return true unless sideload
203
+
204
+ sideload.scope_proc.nil? &&
205
+ sideload.params_proc.nil? &&
206
+ !sideload.customized_base_scope? &&
207
+ sideload.primary_key == :id
208
+ end
209
+
210
+ # Nothing to post to the pool means the future would wrap work already done.
211
+ def applicable_sideloads?
212
+ each_applicable_sideload { return true }
213
+ false
214
+ end
215
+
176
216
  def each_applicable_sideload
177
217
  @query.sideloads.each_pair do |name, sideload_query|
178
218
  sideload = @resource.class.sideload(name)
@@ -194,6 +234,8 @@ module Graphiti
194
234
  sideload_promises << promise.flat
195
235
  end
196
236
 
237
+ return sideload_promises.first if sideload_promises.one?
238
+
197
239
  Concurrent::Promises.zip_futures_on(self.class.global_thread_pool_executor, *sideload_promises)
198
240
  .rescue_on(self.class.global_thread_pool_executor) do |*reasons|
199
241
  first_error = reasons.find { |r| r.is_a?(Exception) }
@@ -202,7 +244,10 @@ module Graphiti
202
244
  end
203
245
 
204
246
  def future_with_context(*args)
247
+ # TODO: we only need Fiber.storage after Ruby 3.2 is the floor
205
248
  thread_storage = Thread.current.keys.each_with_object({}) do |key, memo|
249
+ next if key == POOL_THREAD
250
+
206
251
  memo[key] = Thread.current[key]
207
252
  end
208
253
  fiber_storage =
@@ -215,11 +260,13 @@ module Graphiti
215
260
  Concurrent::Promises.future_on(
216
261
  self.class.global_thread_pool_executor, Thread.current.object_id, thread_storage, fiber_storage, *args
217
262
  ) do |thread_id, thread_storage, fiber_storage, *args|
218
- wrap_in_rails_executor do
219
- with_thread_locals(thread_storage) do
220
- with_fiber_locals(fiber_storage) do
221
- Graphiti.broadcast(:global_thread_pool_task_run, self.class.global_thread_pool_stats) do
222
- yield(*args)
263
+ self.class.marking_pool_thread do
264
+ wrap_in_rails_executor do
265
+ with_thread_locals(thread_storage) do
266
+ 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
+ end
223
270
  end
224
271
  end
225
272
  end
@@ -108,10 +108,10 @@ class Graphiti::Sideload::PolymorphicBelongsTo < Graphiti::Sideload::BelongsTo
108
108
  end
109
109
 
110
110
  def resolve(parents, query, graph_parent)
111
- if Graphiti.config.concurrency
112
- future_resolve(parents, query, graph_parent).value!
113
- else
111
+ if ::Graphiti::Scope.resolve_synchronously?
114
112
  sync_resolve(parents, query, graph_parent)
113
+ else
114
+ future_resolve(parents, query, graph_parent).value!
115
115
  end
116
116
  end
117
117
 
@@ -120,7 +120,14 @@ class Graphiti::Sideload::PolymorphicBelongsTo < Graphiti::Sideload::BelongsTo
120
120
  each_resolvable_group(parents, query) do |child, group, child_query|
121
121
  promises << child.future_resolve(group, child_query, graph_parent)
122
122
  end
123
- Concurrent::Promises.zip(*promises)
123
+ return promises.first if promises.one?
124
+
125
+ executor = ::Graphiti::Scope.global_thread_pool_executor
126
+ Concurrent::Promises.zip_futures_on(executor, *promises)
127
+ .rescue_on(executor) do |*reasons|
128
+ first_error = reasons.find { |reason| reason.is_a?(Exception) }
129
+ raise first_error
130
+ end
124
131
  end
125
132
 
126
133
  private
@@ -280,9 +280,11 @@ module Graphiti
280
280
  end
281
281
 
282
282
  def load(parents, query, graph_parent)
283
- return build_resource_proxy(parents, query, graph_parent).to_a unless Graphiti.config.concurrency
284
-
285
- future_load(parents, query, graph_parent).value!
283
+ if Scope.resolve_synchronously?
284
+ build_resource_proxy(parents, query, graph_parent).to_a
285
+ else
286
+ future_load(parents, query, graph_parent).value!
287
+ end
286
288
  end
287
289
 
288
290
  # Override in subclass
@@ -333,10 +335,10 @@ module Graphiti
333
335
  end
334
336
 
335
337
  def resolve(parents, query, graph_parent)
336
- if Graphiti.config.concurrency
337
- future_resolve(parents, query, graph_parent).value!
338
- else
338
+ if Scope.resolve_synchronously?
339
339
  sync_resolve(parents, query, graph_parent)
340
+ else
341
+ future_resolve(parents, query, graph_parent).value!
340
342
  end
341
343
  end
342
344
 
@@ -57,6 +57,7 @@ module Graphiti
57
57
  def block
58
58
  link_ref = link?
59
59
  sideload_ref = @sideload
60
+ resource_class_ref = @resource_class
60
61
  data_proc_ref = data_proc
61
62
  self_ref = self
62
63
  validate_link! if eagerly_validate_links?
@@ -71,7 +72,15 @@ module Graphiti
71
72
  if sideload_ref.resource_ids_from_foreign_key? &&
72
73
  !self_ref.send(:included_anywhere?, @proxy.query.include_hash, sideload_ref.name)
73
74
  linkage always: sideload_ref.render_resource_ids? do
74
- foreign_key = @object.public_send(sideload_ref.foreign_key)
75
+ foreign_key = begin
76
+ @object.public_send(sideload_ref.foreign_key)
77
+ rescue NoMethodError => error
78
+ raise unless defined?(ActiveModel::MissingAttributeError) &&
79
+ error.is_a?(ActiveModel::MissingAttributeError)
80
+
81
+ raise Errors::UnselectedForeignKey
82
+ .new(resource_class_ref, sideload_ref, @object)
83
+ end
75
84
 
76
85
  unless foreign_key.nil?
77
86
  {
@@ -1,3 +1,3 @@
1
1
  module Graphiti
2
- VERSION = "2.0.0.beta.8"
2
+ VERSION = "2.0.0.beta.9"
3
3
  end
data/lib/graphiti.rb CHANGED
@@ -33,7 +33,7 @@ require "jsonapi/serializable"
33
33
  # either way, whichever copy wins.
34
34
  {
35
35
  "graphiti_spec_helpers" => 'The "graphiti_spec_helpers/rspec" require and the GraphitiSpecHelpers namespace are unchanged.',
36
- "graphiti-rails" => 'Graphiti::Rails and its config.graphiti options are unchanged. Drop the "graphiti-rails" require if you have one.',
36
+ "graphiti-rails" => 'Graphiti::Rails and its config.graphiti options are unchanged. Drop the "graphiti-rails" require if you have one, and add `include Graphiti::Rails::Controller` to controllers serving Graphiti resources. graphiti-rails installed that on every controller automatically. See graphiti.dev/upgrading.',
37
37
  "graphiti_errors" => "Exception handling now goes through rescue_registry. Remove `include GraphitiErrors` from your controllers — Graphiti registers its own handlers, and you can add yours with `register_exception`."
38
38
  }.each do |gem_name, guidance|
39
39
  next unless Gem.loaded_specs.key?(gem_name)
data/package-lock.json CHANGED
@@ -10,6 +10,7 @@
10
10
  "license": "MIT",
11
11
  "devDependencies": {
12
12
  "@semantic-release/changelog": "^7.0.0",
13
+ "@semantic-release/exec": "^7.1.0",
13
14
  "@semantic-release/git": "^11.0.1",
14
15
  "semantic-release": "^25.0.8",
15
16
  "semantic-release-rubygem": "^1.2.0"
@@ -394,6 +395,119 @@
394
395
  "node": ">=18"
395
396
  }
396
397
  },
398
+ "node_modules/@semantic-release/exec": {
399
+ "version": "7.1.0",
400
+ "resolved": "https://registry.npmjs.org/@semantic-release/exec/-/exec-7.1.0.tgz",
401
+ "integrity": "sha512-4ycZ2atgEUutspPZ2hxO6z8JoQt4+y/kkHvfZ1cZxgl9WKJId1xPj+UadwInj+gMn2Gsv+fLnbrZ4s+6tK2TFQ==",
402
+ "dev": true,
403
+ "license": "MIT",
404
+ "dependencies": {
405
+ "@semantic-release/error": "^4.0.0",
406
+ "aggregate-error": "^3.0.0",
407
+ "debug": "^4.0.0",
408
+ "execa": "^9.0.0",
409
+ "lodash-es": "^4.17.21",
410
+ "parse-json": "^8.0.0"
411
+ },
412
+ "engines": {
413
+ "node": ">=20.8.1"
414
+ },
415
+ "peerDependencies": {
416
+ "semantic-release": ">=24.1.0"
417
+ }
418
+ },
419
+ "node_modules/@semantic-release/exec/node_modules/aggregate-error": {
420
+ "version": "3.1.0",
421
+ "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
422
+ "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
423
+ "dev": true,
424
+ "license": "MIT",
425
+ "dependencies": {
426
+ "clean-stack": "^2.0.0",
427
+ "indent-string": "^4.0.0"
428
+ },
429
+ "engines": {
430
+ "node": ">=8"
431
+ }
432
+ },
433
+ "node_modules/@semantic-release/exec/node_modules/clean-stack": {
434
+ "version": "2.2.0",
435
+ "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
436
+ "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
437
+ "dev": true,
438
+ "license": "MIT",
439
+ "engines": {
440
+ "node": ">=6"
441
+ }
442
+ },
443
+ "node_modules/@semantic-release/exec/node_modules/execa": {
444
+ "version": "9.6.1",
445
+ "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz",
446
+ "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==",
447
+ "dev": true,
448
+ "license": "MIT",
449
+ "dependencies": {
450
+ "@sindresorhus/merge-streams": "^4.0.0",
451
+ "cross-spawn": "^7.0.6",
452
+ "figures": "^6.1.0",
453
+ "get-stream": "^9.0.0",
454
+ "human-signals": "^8.0.1",
455
+ "is-plain-obj": "^4.1.0",
456
+ "is-stream": "^4.0.1",
457
+ "npm-run-path": "^6.0.0",
458
+ "pretty-ms": "^9.2.0",
459
+ "signal-exit": "^4.1.0",
460
+ "strip-final-newline": "^4.0.0",
461
+ "yoctocolors": "^2.1.1"
462
+ },
463
+ "engines": {
464
+ "node": "^18.19.0 || >=20.5.0"
465
+ },
466
+ "funding": {
467
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
468
+ }
469
+ },
470
+ "node_modules/@semantic-release/exec/node_modules/indent-string": {
471
+ "version": "4.0.0",
472
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
473
+ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
474
+ "dev": true,
475
+ "license": "MIT",
476
+ "engines": {
477
+ "node": ">=8"
478
+ }
479
+ },
480
+ "node_modules/@semantic-release/exec/node_modules/parse-json": {
481
+ "version": "8.3.0",
482
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz",
483
+ "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==",
484
+ "dev": true,
485
+ "license": "MIT",
486
+ "dependencies": {
487
+ "@babel/code-frame": "^7.26.2",
488
+ "index-to-position": "^1.1.0",
489
+ "type-fest": "^4.39.1"
490
+ },
491
+ "engines": {
492
+ "node": ">=18"
493
+ },
494
+ "funding": {
495
+ "url": "https://github.com/sponsors/sindresorhus"
496
+ }
497
+ },
498
+ "node_modules/@semantic-release/exec/node_modules/type-fest": {
499
+ "version": "4.41.0",
500
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
501
+ "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
502
+ "dev": true,
503
+ "license": "(MIT OR CC0-1.0)",
504
+ "engines": {
505
+ "node": ">=16"
506
+ },
507
+ "funding": {
508
+ "url": "https://github.com/sponsors/sindresorhus"
509
+ }
510
+ },
397
511
  "node_modules/@semantic-release/git": {
398
512
  "version": "11.0.1",
399
513
  "resolved": "https://registry.npmjs.org/@semantic-release/git/-/git-11.0.1.tgz",
data/package.json CHANGED
@@ -18,7 +18,8 @@
18
18
  "semantic-release-rubygem": "^1.2.0",
19
19
  "semantic-release": "^25.0.8",
20
20
  "@semantic-release/changelog": "^7.0.0",
21
- "@semantic-release/git": "^11.0.1"
21
+ "@semantic-release/git": "^11.0.1",
22
+ "@semantic-release/exec": "^7.1.0"
22
23
  },
23
24
  "release": {
24
25
  "branches": [
@@ -132,13 +133,25 @@
132
133
  }
133
134
  ],
134
135
  "semantic-release-rubygem",
135
- "@semantic-release/github",
136
+ [
137
+ "@semantic-release/github",
138
+ {
139
+ "successCommentCondition": "<% return !branch.prerelease; %>"
140
+ }
141
+ ],
142
+ [
143
+ "@semantic-release/exec",
144
+ {
145
+ "prepareCmd": "bundle exec ruby spec/performance/measure_releases.rb --pending v${nextRelease.version}"
146
+ }
147
+ ],
136
148
  [
137
149
  "@semantic-release/git",
138
150
  {
139
151
  "assets": [
140
152
  "CHANGELOG.md",
141
- "lib/graphiti/version.rb"
153
+ "lib/graphiti/version.rb",
154
+ "spec/performance/performance_history.tsv"
142
155
  ],
143
156
  "message": "${nextRelease.version} CHANGELOG [skip ci]\n\n${nextRelease.notes}"
144
157
  }
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.8
4
+ version: 2.0.0.beta.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lee Richmond
@@ -340,6 +340,7 @@ files:
340
340
  - lib/graphiti/rails/rake_helpers.rb
341
341
  - lib/graphiti/rails/responders.rb
342
342
  - lib/graphiti/rails/test_helpers.rb
343
+ - lib/graphiti/railtie.rb
343
344
  - lib/graphiti/renderer.rb
344
345
  - lib/graphiti/request_validator.rb
345
346
  - lib/graphiti/request_validators/update_validator.rb
@@ -428,7 +429,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
428
429
  - !ruby/object:Gem::Version
429
430
  version: '0'
430
431
  requirements: []
431
- rubygems_version: 3.6.9
432
+ rubygems_version: 4.0.16
432
433
  specification_version: 4
433
434
  summary: Easily build jsonapi.org-compatible APIs
434
435
  test_files: []