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 +4 -4
- data/.github/workflows/ci.yml +1 -1
- data/.github/workflows/docs.yml +9 -6
- data/.github/workflows/release.yml +1 -1
- data/CHANGELOG.md +21 -0
- data/Rakefile +28 -0
- data/UPGRADING.md +1 -1
- data/lib/graphiti/configuration.rb +11 -4
- data/lib/graphiti/debugger.rb +18 -6
- data/lib/graphiti/error_serializers/deprecated_constants.rb +23 -0
- data/lib/graphiti/error_serializers/invalid_request.rb +6 -0
- data/lib/graphiti/errors.rb +16 -0
- data/lib/graphiti/query.rb +2 -1
- data/lib/graphiti/rails.rb +14 -0
- data/lib/graphiti/railtie.rb +8 -0
- data/lib/graphiti/scope.rb +68 -21
- data/lib/graphiti/sideload/polymorphic_belongs_to.rb +11 -4
- data/lib/graphiti/sideload.rb +8 -6
- data/lib/graphiti/util/serializer_relationships.rb +10 -1
- data/lib/graphiti/version.rb +1 -1
- data/lib/graphiti.rb +1 -1
- data/package-lock.json +114 -0
- data/package.json +16 -3
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a61d00c6968d2dc24afb8986d7b3b6051394ea73ac5d5a8104f1296f650fef47
|
|
4
|
+
data.tar.gz: bf367c2e3ff33e4240a3f11e3fbae780c75b7f727863e61fc0c546ae9cdfb5fe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4bb22545b0a2b590fb07a237857407a5a50ac2423dd33a5621af164d93b4d36237f54339fb083895e6d349ae64da3cf9cc2e24c363f63478cea691622f130331
|
|
7
|
+
data.tar.gz: 9da999b97fa15b04ca97420d6a6942c5d32b70bd0e8f27e33e01b8861cdd0206342d6afd11db15eb0c1d445beda4ce1f6a5ff5cceffcb54d5917e3a185cdc16a
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -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
|
data/.github/workflows/docs.yml
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
name: Docs
|
|
2
2
|
|
|
3
|
-
# Publishes the documentation site from beta
|
|
4
|
-
# repo via website/static/CNAME, so this
|
|
5
|
-
#
|
|
6
|
-
# and graphiti-api.github.io no
|
|
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
|
|
20
|
+
group: docs
|
|
18
21
|
cancel-in-progress: true
|
|
19
22
|
|
|
20
23
|
permissions:
|
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
|
|
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 =
|
|
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 =
|
|
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)
|
data/lib/graphiti/debugger.rb
CHANGED
|
@@ -4,11 +4,22 @@
|
|
|
4
4
|
module Graphiti
|
|
5
5
|
class Debugger
|
|
6
6
|
class << self
|
|
7
|
-
attr_accessor :enabled, :
|
|
7
|
+
attr_accessor :enabled, :debug_models, :preserve, :pry
|
|
8
8
|
end
|
|
9
|
-
|
|
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
|
-
|
|
193
|
+
statements = chunks
|
|
194
|
+
statements.each do |chunk|
|
|
183
195
|
if (parent = chunk[:parent])
|
|
184
|
-
relevant =
|
|
196
|
+
relevant = statements.find { |c| c[:resource] == parent }
|
|
185
197
|
relevant[:children].unshift(chunk) if relevant
|
|
186
198
|
end
|
|
187
199
|
end
|
|
188
|
-
|
|
189
|
-
|
|
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
|
data/lib/graphiti/errors.rb
CHANGED
|
@@ -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
|
data/lib/graphiti/query.rb
CHANGED
|
@@ -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
|
|
36
|
+
@entity_map
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
def top_level?
|
data/lib/graphiti/rails.rb
CHANGED
|
@@ -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
|
+
)
|
data/lib/graphiti/scope.rb
CHANGED
|
@@ -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
|
-
#
|
|
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
|
|
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
|
|
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
|
-
|
|
172
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
data/lib/graphiti/sideload.rb
CHANGED
|
@@ -280,9 +280,11 @@ module Graphiti
|
|
|
280
280
|
end
|
|
281
281
|
|
|
282
282
|
def load(parents, query, graph_parent)
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
|
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 =
|
|
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
|
{
|
data/lib/graphiti/version.rb
CHANGED
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
|
-
|
|
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.
|
|
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:
|
|
432
|
+
rubygems_version: 4.0.16
|
|
432
433
|
specification_version: 4
|
|
433
434
|
summary: Easily build jsonapi.org-compatible APIs
|
|
434
435
|
test_files: []
|