graphiti 2.0.0.beta.10 → 2.0.0.beta.11
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/release.yml +2 -0
- data/.tool-versions +1 -1
- data/CHANGELOG.md +24 -0
- data/Rakefile +17 -12
- data/lib/graphiti/adapters/abstract.rb +4 -3
- data/lib/graphiti/errors.rb +4 -4
- data/lib/graphiti/query.rb +27 -0
- data/lib/graphiti/renderer.rb +4 -1
- data/lib/graphiti/resource/configuration.rb +11 -10
- data/lib/graphiti/resource/dsl.rb +5 -5
- data/lib/graphiti/resource/links.rb +3 -0
- data/lib/graphiti/scope.rb +51 -22
- data/lib/graphiti/scoping/filter.rb +2 -2
- data/lib/graphiti/serializer.rb +25 -9
- data/lib/graphiti/sideload/polymorphic_belongs_to.rb +9 -9
- data/lib/graphiti/sideload.rb +36 -25
- data/lib/graphiti/spec_helpers/matchers.rb +1 -1
- data/lib/graphiti/version.rb +1 -1
- data/package.json +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a639e421891e5d5e650d2d8131a75905cb9ffd0fb40cdfd15239ca5caf29d12f
|
|
4
|
+
data.tar.gz: 95172bbff224d598c959d5acc93c30861e01c50540fdea871ea5029473bf5259
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 24c275cfcaecdba9ced126b7970b37962998673a3a8609c885a69befc53adba646ec2c7d9517198509e358ebba5300f3b336c86f02df98af9d18a6f4a4c306c3
|
|
7
|
+
data.tar.gz: e7d2fd4e0d0ff74b68ea4d3673399d1b72897ad7c8647fa38835ff2b58c850d58ba95a1145a52f55893e88e36eb0aa90bece72a2e6e593173661d16100666edc
|
data/.tool-versions
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
ruby
|
|
1
|
+
ruby 4.0.6
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
graphiti changelog
|
|
2
2
|
|
|
3
|
+
# [2.0.0-beta.11](https://github.com/graphiti-api/graphiti/compare/v2.0.0-beta.10...v2.0.0-beta.11) (2026-08-27)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* always render included when the client asked to include ([8691b3f](https://github.com/graphiti-api/graphiti/commit/8691b3fd08f3cae0481ba72839911fdc71ef5634))
|
|
9
|
+
* autolink = true no longer clobbers an inherited :on_demand ([2d26f8c](https://github.com/graphiti-api/graphiti/commit/2d26f8cb567e82a16918345de9114b8ceef64523))
|
|
10
|
+
* read the links param from the query, not the context object ([3b934cb](https://github.com/graphiti-api/graphiti/commit/3b934cb25be049e1e80ba400123da944cfd23d04))
|
|
11
|
+
* stop assigning the same child twice when a resource repeats in the include path ([588503b](https://github.com/graphiti-api/graphiti/commit/588503b109508495d8170373d80019c6971f7d9c))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
* omit relationships that render no ids and no link ([916aa8c](https://github.com/graphiti-api/graphiti/commit/916aa8c9c596c5d9b6c802c5ab13b766a13d1e70))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Performance Improvements
|
|
20
|
+
|
|
21
|
+
* memoize whether a belongs_to renders ids from its foreign key ([dcb60b2](https://github.com/graphiti-api/graphiti/commit/dcb60b2096c414756ccff534773d2e45594f0144))
|
|
22
|
+
* only deduplicate resources that two include paths can both reach ([744f014](https://github.com/graphiti-api/graphiti/commit/744f014249382bc25d5ea43f692778ad7adc25c3))
|
|
23
|
+
* resolve a lone sideload inline rather than through the pool ([3415592](https://github.com/graphiti-api/graphiti/commit/3415592d75900389a8f11f53b65ab399e297e4a9))
|
|
24
|
+
* reuse the resolved data and proxies when building cache keys ([8a17350](https://github.com/graphiti-api/graphiti/commit/8a17350542ba0062fdcddd115afe8041b62198c7))
|
|
25
|
+
* skip the on-demand link strip when no relationship renders links on demand ([cf86a8c](https://github.com/graphiti-api/graphiti/commit/cf86a8c249077d9f9d5a13de49989d102bbaed04))
|
|
26
|
+
|
|
3
27
|
# [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
28
|
|
|
5
29
|
|
data/Rakefile
CHANGED
|
@@ -11,31 +11,36 @@ RSpec::Core::RakeTask.new(:spec) do |t|
|
|
|
11
11
|
end
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
# Allocations mean the same anywhere and timings do not, so the history has a
|
|
15
|
+
# reference CPU, named on every row, and recording timings refuses to mix them.
|
|
14
16
|
namespace :performance do
|
|
15
17
|
# Shelling out keeps a failure to the script's own message, with no rake backtrace on top.
|
|
16
18
|
def measure(*arguments)
|
|
17
19
|
exit(1) unless system("bundle", "exec", "ruby", "spec/performance/measure_releases.rb", *arguments)
|
|
18
20
|
end
|
|
19
21
|
|
|
20
|
-
desc "
|
|
21
|
-
task :
|
|
22
|
-
measure
|
|
22
|
+
desc "Read a change: measure the working tree, print the comparison and plot it"
|
|
23
|
+
task :read do
|
|
24
|
+
measure("--read")
|
|
23
25
|
end
|
|
24
26
|
|
|
25
|
-
desc "
|
|
26
|
-
task :
|
|
27
|
-
exit(1) unless system("bundle", "exec", "ruby", "spec/performance/chart_page.rb")
|
|
27
|
+
desc "Open the recorded history as a chart. Any machine, measures nothing"
|
|
28
|
+
task :chart do
|
|
29
|
+
exit(1) unless system("bundle", "exec", "ruby", "spec/performance/chart_page.rb", "--no-current")
|
|
28
30
|
end
|
|
29
31
|
|
|
30
|
-
desc "
|
|
32
|
+
desc "Fill in the history. Missing releases by default, TAG=v2.0.0-beta.9 for one, ALL=1 for every. Reference CPU"
|
|
31
33
|
task :record do
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
measure(*(if ENV["TAG"]
|
|
35
|
+
[ENV["TAG"]]
|
|
36
|
+
else
|
|
37
|
+
ENV["ALL"] ? ["--all"] : ["--missing"]
|
|
38
|
+
end))
|
|
34
39
|
end
|
|
35
40
|
|
|
36
|
-
desc "
|
|
37
|
-
task :
|
|
38
|
-
measure("--
|
|
41
|
+
desc "Measure the working tree for the release to name, and commit it. Reference CPU"
|
|
42
|
+
task :pending do
|
|
43
|
+
measure("--pending")
|
|
39
44
|
end
|
|
40
45
|
end
|
|
41
46
|
|
|
@@ -371,10 +371,11 @@ module Graphiti
|
|
|
371
371
|
activerecord_adapter.associate \
|
|
372
372
|
parent, child, association_name, association_type
|
|
373
373
|
elsif [:has_many, :many_to_many].include?(association_type)
|
|
374
|
-
|
|
374
|
+
associated = parent.send(:"#{association_name}")
|
|
375
|
+
if associated.nil?
|
|
375
376
|
parent.send(:"#{association_name}=", [child])
|
|
376
|
-
|
|
377
|
-
|
|
377
|
+
elsif associated.none? { |existing| existing.equal?(child) }
|
|
378
|
+
associated << child
|
|
378
379
|
end
|
|
379
380
|
else
|
|
380
381
|
parent.send(:"#{association_name}=", child)
|
data/lib/graphiti/errors.rb
CHANGED
|
@@ -719,11 +719,11 @@ module Graphiti
|
|
|
719
719
|
|
|
720
720
|
def message
|
|
721
721
|
<<~MSG
|
|
722
|
-
#{@resource_class.name}: #{@attribute} must be one of :
|
|
722
|
+
#{@resource_class.name}: #{@attribute} must be one of :literal, :null, or :rejected. Got #{@value.inspect}.
|
|
723
723
|
|
|
724
|
-
:
|
|
725
|
-
:
|
|
726
|
-
:
|
|
724
|
+
:literal - "null" and "" reach the filter as strings (default)
|
|
725
|
+
:null - "null" becomes nil, so the filter can query for NULL
|
|
726
|
+
:rejected - a blank value raises InvalidFilterValue
|
|
727
727
|
MSG
|
|
728
728
|
end
|
|
729
729
|
end
|
data/lib/graphiti/query.rb
CHANGED
|
@@ -39,10 +39,37 @@ module Graphiti
|
|
|
39
39
|
@entity_map
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
# Deduplication exists for a row two include paths both resolve, so a
|
|
43
|
+
# resource class sitting at one node has nothing to collide with.
|
|
44
|
+
def repeated_resource_classes
|
|
45
|
+
return root.repeated_resource_classes unless root == self
|
|
46
|
+
|
|
47
|
+
@repeated_resource_classes ||= begin
|
|
48
|
+
counts = Hash.new(0)
|
|
49
|
+
walk = [self]
|
|
50
|
+
while (query = walk.pop)
|
|
51
|
+
counts[query.resource.class] += 1
|
|
52
|
+
query.sideloads.each_pair do |name, sideload_query|
|
|
53
|
+
walk << sideload_query
|
|
54
|
+
sideload = query.resource.class.sideload(name)
|
|
55
|
+
next unless sideload.respond_to?(:children)
|
|
56
|
+
|
|
57
|
+
# A polymorphic child resolves under a class no node names.
|
|
58
|
+
sideload.children.each_value { |child| counts[child.resource.class] += 2 }
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
counts.select { |_, count| count > 1 }.keys.to_set
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
42
65
|
def top_level?
|
|
43
66
|
!association?
|
|
44
67
|
end
|
|
45
68
|
|
|
69
|
+
def includes_requested?
|
|
70
|
+
@include_param.present?
|
|
71
|
+
end
|
|
72
|
+
|
|
46
73
|
def links_requested?
|
|
47
74
|
return @links_requested if defined?(@links_requested)
|
|
48
75
|
|
data/lib/graphiti/renderer.rb
CHANGED
|
@@ -13,8 +13,11 @@ module Graphiti
|
|
|
13
13
|
@records ||= @proxy.data
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
# JSON:API requires the key even when the include turns up nothing.
|
|
16
17
|
def to_jsonapi
|
|
17
|
-
render(self.class.jsonapi_renderer)
|
|
18
|
+
rendered = render(self.class.jsonapi_renderer)
|
|
19
|
+
rendered[:included] ||= [] if proxy.query.includes_requested?
|
|
20
|
+
rendered.to_json
|
|
18
21
|
end
|
|
19
22
|
|
|
20
23
|
def as_graphql
|
|
@@ -6,7 +6,7 @@ module Graphiti
|
|
|
6
6
|
DEFAULT_MAX_PAGE_SIZE = 1_000
|
|
7
7
|
LINK_MODES = [true, false, :on_demand].freeze
|
|
8
8
|
BELONGS_TO_RESOURCE_IDS_MODES = [:foreign_key, :always, :never].freeze
|
|
9
|
-
BLANK_MODES = [:
|
|
9
|
+
BLANK_MODES = [:literal, :null, :rejected].freeze
|
|
10
10
|
|
|
11
11
|
# Grouped for the ApplicationResource the install generator writes.
|
|
12
12
|
SETTING_GROUPS = { # :nodoc:
|
|
@@ -21,6 +21,7 @@ module Graphiti
|
|
|
21
21
|
relationships: {
|
|
22
22
|
relationships_readable_by_default: {default: true},
|
|
23
23
|
relationships_writable_by_default: {default: true},
|
|
24
|
+
relationship_placeholders: {default: false, note: "render placeholders for relationships with no ids and no link"},
|
|
24
25
|
belongs_to_resource_ids_by_default: {
|
|
25
26
|
default: :foreign_key,
|
|
26
27
|
values: BELONGS_TO_RESOURCE_IDS_MODES,
|
|
@@ -28,10 +29,10 @@ module Graphiti
|
|
|
28
29
|
}
|
|
29
30
|
},
|
|
30
31
|
filters: {
|
|
31
|
-
|
|
32
|
-
default: :
|
|
32
|
+
filter_blanks_treated_as: {
|
|
33
|
+
default: :literal,
|
|
33
34
|
values: BLANK_MODES,
|
|
34
|
-
invalid: ->(klass, value) { Errors::InvalidFilterBlanks.new(klass, :
|
|
35
|
+
invalid: ->(klass, value) { Errors::InvalidFilterBlanks.new(klass, :filter_blanks_treated_as, value) }
|
|
35
36
|
}
|
|
36
37
|
},
|
|
37
38
|
sorting: {
|
|
@@ -194,21 +195,21 @@ module Graphiti
|
|
|
194
195
|
end
|
|
195
196
|
|
|
196
197
|
class_methods do
|
|
197
|
-
# Deprecated. Both folded into
|
|
198
|
+
# Deprecated. Both folded into filter_blanks_treated_as. Remove in 3.0.
|
|
198
199
|
def filters_accept_nil_by_default
|
|
199
|
-
|
|
200
|
+
filter_blanks_treated_as == :null
|
|
200
201
|
end
|
|
201
202
|
|
|
202
203
|
def filters_accept_nil_by_default=(val)
|
|
203
|
-
self.
|
|
204
|
+
self.filter_blanks_treated_as = val ? :null : :literal
|
|
204
205
|
end
|
|
205
206
|
|
|
206
207
|
def filters_deny_empty_by_default
|
|
207
|
-
|
|
208
|
+
filter_blanks_treated_as == :rejected
|
|
208
209
|
end
|
|
209
210
|
|
|
210
211
|
def filters_deny_empty_by_default=(val)
|
|
211
|
-
self.
|
|
212
|
+
self.filter_blanks_treated_as = val ? :rejected : :literal
|
|
212
213
|
end
|
|
213
214
|
|
|
214
215
|
# Deprecated. Renamed to the page_ family. Remove in 3.0.
|
|
@@ -416,7 +417,7 @@ module Graphiti
|
|
|
416
417
|
end
|
|
417
418
|
end
|
|
418
419
|
|
|
419
|
-
blanks_msg = "Use `self.
|
|
420
|
+
blanks_msg = "Use `self.filter_blanks_treated_as` (:literal, :null, or :rejected)"
|
|
420
421
|
page_msg = "Use `self.page_default_size`, `self.page_max_size` and `self.page_cursors`"
|
|
421
422
|
DEPRECATOR.deprecate_methods(Configuration::ClassMethods,
|
|
422
423
|
filters_accept_nil_by_default: blanks_msg,
|
|
@@ -208,16 +208,16 @@ module Graphiti
|
|
|
208
208
|
|
|
209
209
|
def blanks_for(name, opts)
|
|
210
210
|
if opts.key?(:deny_empty)
|
|
211
|
-
DEPRECATOR.warn("The deny_empty: filter option is deprecated. Use blanks: :
|
|
212
|
-
return :
|
|
211
|
+
DEPRECATOR.warn("The deny_empty: filter option is deprecated. Use blanks: :rejected.")
|
|
212
|
+
return :rejected if opts[:deny_empty]
|
|
213
213
|
end
|
|
214
214
|
|
|
215
215
|
if opts.key?(:allow_nil)
|
|
216
|
-
DEPRECATOR.warn("The allow_nil: filter option is deprecated. Use blanks: :
|
|
217
|
-
return opts[:allow_nil] ? :
|
|
216
|
+
DEPRECATOR.warn("The allow_nil: filter option is deprecated. Use blanks: :null.")
|
|
217
|
+
return opts[:allow_nil] ? :null : :literal
|
|
218
218
|
end
|
|
219
219
|
|
|
220
|
-
blanks = opts.fetch(:blanks,
|
|
220
|
+
blanks = opts.fetch(:blanks, filter_blanks_treated_as)
|
|
221
221
|
unless Resource::BLANK_MODES.include?(blanks)
|
|
222
222
|
raise Errors::InvalidFilterBlanks.new(self, name, blanks)
|
|
223
223
|
end
|
|
@@ -39,7 +39,10 @@ module Graphiti
|
|
|
39
39
|
relationship_links != false
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
# true cannot express :on_demand, so it leaves an inherited on-demand mode alone.
|
|
42
43
|
def autolink=(val)
|
|
44
|
+
return if val && relationship_links == :on_demand
|
|
45
|
+
|
|
43
46
|
self.relationship_links = val ? true : false
|
|
44
47
|
end
|
|
45
48
|
|
data/lib/graphiti/scope.rb
CHANGED
|
@@ -59,6 +59,9 @@ module Graphiti
|
|
|
59
59
|
@query = query
|
|
60
60
|
@opts = opts
|
|
61
61
|
|
|
62
|
+
@resolved_sideload_proxies = {}
|
|
63
|
+
@resolved_sideload_proxies_lock = Mutex.new
|
|
64
|
+
|
|
62
65
|
@object = @resource.around_scoping(@object, @query.hash) { |scope|
|
|
63
66
|
apply_scoping(scope, opts)
|
|
64
67
|
}
|
|
@@ -67,7 +70,7 @@ module Graphiti
|
|
|
67
70
|
def resolve(&blk)
|
|
68
71
|
# The caller blocks on .value! either way, so concurrency only benefits parallel sideloads
|
|
69
72
|
# See https://github.com/graphiti-api/graphiti/issues/505
|
|
70
|
-
if self.class.resolve_synchronously? || !
|
|
73
|
+
if self.class.resolve_synchronously? || !overlapping_sideloads?
|
|
71
74
|
sync_resolve(&blk)
|
|
72
75
|
else
|
|
73
76
|
future_resolve(&blk).value!
|
|
@@ -140,9 +143,6 @@ module Graphiti
|
|
|
140
143
|
|
|
141
144
|
private
|
|
142
145
|
|
|
143
|
-
# Synchronous counterpart to #future_resolve, used when concurrency is off.
|
|
144
|
-
# Resolves the resource and its sideloads inline without any promise
|
|
145
|
-
# machinery. See #resolve.
|
|
146
146
|
def sync_resolve(&blk)
|
|
147
147
|
return [] if @query.zero_results?
|
|
148
148
|
|
|
@@ -151,14 +151,15 @@ module Graphiti
|
|
|
151
151
|
resolved
|
|
152
152
|
end
|
|
153
153
|
|
|
154
|
-
# Synchronous counterpart to #future_resolve_sideloads, used when
|
|
155
|
-
# concurrency is off. Resolves each sideload inline. See #resolve_sideloads.
|
|
156
154
|
def sync_resolve_sideloads(results)
|
|
157
155
|
return if results == []
|
|
158
156
|
|
|
159
|
-
|
|
157
|
+
reset_captured_sideload_proxies
|
|
158
|
+
each_applicable_sideload do |name, sideload, sideload_query|
|
|
160
159
|
Graphiti.config.before_sideload&.call(Graphiti.context)
|
|
161
|
-
sideload.
|
|
160
|
+
sideload.sync_resolve(results, sideload_query, @resource) do |proxy|
|
|
161
|
+
capture_sideload_proxy(name, proxy)
|
|
162
|
+
end
|
|
162
163
|
end
|
|
163
164
|
end
|
|
164
165
|
|
|
@@ -176,7 +177,7 @@ module Graphiti
|
|
|
176
177
|
assign_serializer(resolved)
|
|
177
178
|
yield resolved if block_given?
|
|
178
179
|
@opts[:after_resolve]&.call(resolved)
|
|
179
|
-
resolved
|
|
180
|
+
@resolved_records = resolved
|
|
180
181
|
end
|
|
181
182
|
|
|
182
183
|
# Must run before sideloads assign, so every include path populates the
|
|
@@ -198,6 +199,8 @@ module Graphiti
|
|
|
198
199
|
|
|
199
200
|
# A customized sideload can load a record another path would not, so it keeps its own instances.
|
|
200
201
|
def deduplicable?
|
|
202
|
+
return false unless @query.repeated_resource_classes.include?(@resource.class)
|
|
203
|
+
|
|
201
204
|
sideload = @opts[:sideload]
|
|
202
205
|
return true unless sideload
|
|
203
206
|
|
|
@@ -207,9 +210,15 @@ module Graphiti
|
|
|
207
210
|
sideload.primary_key == :id
|
|
208
211
|
end
|
|
209
212
|
|
|
210
|
-
#
|
|
211
|
-
|
|
212
|
-
|
|
213
|
+
# One sideload has nothing to run beside it, so the pool would hand the work
|
|
214
|
+
# to another thread and wait for it. A chain is one at every level. A
|
|
215
|
+
# polymorphic sideload counts as its children, which do run beside each other.
|
|
216
|
+
def overlapping_sideloads?
|
|
217
|
+
found = 0
|
|
218
|
+
each_applicable_sideload do |_, sideload, _|
|
|
219
|
+
found += sideload.respond_to?(:children) ? sideload.children.size : 1
|
|
220
|
+
return true if found > 1
|
|
221
|
+
end
|
|
213
222
|
false
|
|
214
223
|
end
|
|
215
224
|
|
|
@@ -218,18 +227,34 @@ module Graphiti
|
|
|
218
227
|
sideload = @resource.class.sideload(name)
|
|
219
228
|
next if sideload.nil? || sideload.shared_remote?
|
|
220
229
|
|
|
221
|
-
yield sideload, sideload_query
|
|
230
|
+
yield name, sideload, sideload_query
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# The write path resolves sideloads twice on one scope, which would double every proxy in the cache key.
|
|
235
|
+
def reset_captured_sideload_proxies
|
|
236
|
+
@resolved_sideload_proxies_lock.synchronize { @resolved_sideload_proxies.clear }
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# A proxy built while resolving is already resolved all the way down.
|
|
240
|
+
def capture_sideload_proxy(name, proxy)
|
|
241
|
+
@resolved_sideload_proxies_lock.synchronize do
|
|
242
|
+
captured = (@resolved_sideload_proxies[name] ||= [])
|
|
243
|
+
captured << proxy unless proxy.nil? || proxy == []
|
|
222
244
|
end
|
|
223
245
|
end
|
|
224
246
|
|
|
225
247
|
def future_resolve_sideloads(results)
|
|
226
248
|
return Concurrent::Promises.fulfilled_future(nil, self.class.global_thread_pool_executor) if results == []
|
|
227
249
|
|
|
250
|
+
reset_captured_sideload_proxies
|
|
228
251
|
sideload_promises = []
|
|
229
|
-
each_applicable_sideload do |sideload, sideload_query|
|
|
252
|
+
each_applicable_sideload do |name, sideload, sideload_query|
|
|
230
253
|
promise = future_with_context(results, sideload_query, @resource) do |parent_results, future_query, parent_resource|
|
|
231
254
|
Graphiti.config.before_sideload&.call(Graphiti.context)
|
|
232
|
-
sideload.future_resolve(parent_results, future_query, parent_resource)
|
|
255
|
+
sideload.future_resolve(parent_results, future_query, parent_resource) do |proxy|
|
|
256
|
+
capture_sideload_proxy(name, proxy)
|
|
257
|
+
end
|
|
233
258
|
end
|
|
234
259
|
sideload_promises << promise.flat
|
|
235
260
|
end
|
|
@@ -361,15 +386,19 @@ module Graphiti
|
|
|
361
386
|
|
|
362
387
|
def sideload_resource_proxies
|
|
363
388
|
@sideload_resource_proxies ||= begin
|
|
364
|
-
|
|
365
|
-
results = @
|
|
389
|
+
# Reached after the response resolved, where resolving again re-applies before_resolve to a mutated scope.
|
|
390
|
+
results = @resolved_records
|
|
391
|
+
if results.nil?
|
|
392
|
+
@object = @resource.before_resolve(@object, @query)
|
|
393
|
+
results = @resource.resolve(@object)
|
|
394
|
+
end
|
|
366
395
|
|
|
367
396
|
[].tap do |proxies|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
397
|
+
each_applicable_sideload do |name, sideload, q|
|
|
398
|
+
captured = @resolved_sideload_proxies[name]
|
|
399
|
+
if captured
|
|
400
|
+
proxies.concat(captured)
|
|
401
|
+
else
|
|
373
402
|
proxies << sideload.build_resource_proxy(results, q, parent_resource)
|
|
374
403
|
end
|
|
375
404
|
end
|
|
@@ -209,7 +209,7 @@ module Graphiti
|
|
|
209
209
|
end
|
|
210
210
|
|
|
211
211
|
def parse_string_null(filter, value)
|
|
212
|
-
return value unless filter[:blanks] == :
|
|
212
|
+
return value unless filter[:blanks] == :null
|
|
213
213
|
return value.map { |item| (item == "null") ? nil : item } if value.is_a?(Array)
|
|
214
214
|
return if value == "null"
|
|
215
215
|
|
|
@@ -217,7 +217,7 @@ module Graphiti
|
|
|
217
217
|
end
|
|
218
218
|
|
|
219
219
|
def check_blank_filters!(resource, filter, value)
|
|
220
|
-
return unless filter.values[0][:blanks] == :
|
|
220
|
+
return unless filter.values[0][:blanks] == :rejected
|
|
221
221
|
|
|
222
222
|
if value.nil? || value.empty? || value == "null"
|
|
223
223
|
raise Errors::InvalidFilterValue.new(resource, filter, "(empty)")
|
data/lib/graphiti/serializer.rb
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
module Graphiti
|
|
2
2
|
class Serializer < JSONAPI::Serializable::Resource
|
|
3
|
-
UNREQUESTED_LINKS = [false, nil, "false"].freeze
|
|
4
|
-
|
|
5
3
|
include Graphiti::Extensions::BooleanAttribute
|
|
6
4
|
include Graphiti::Extensions::ExtraAttribute
|
|
7
5
|
include Graphiti::SerializableHash
|
|
@@ -26,6 +24,16 @@ module Graphiti
|
|
|
26
24
|
self.relationship_condition_blocks ||= {}
|
|
27
25
|
self.relationship_sideloads ||= {}
|
|
28
26
|
|
|
27
|
+
# Keyed on the sideloads hash itself, which is reassigned whenever a
|
|
28
|
+
# relationship is applied, so the answer is recomputed exactly then.
|
|
29
|
+
def self.on_demand_links?
|
|
30
|
+
sideloads = relationship_sideloads
|
|
31
|
+
return @on_demand_links.last if @on_demand_links&.first.equal?(sideloads)
|
|
32
|
+
|
|
33
|
+
@on_demand_links = [sideloads, sideloads.each_value.any? { |sideload| sideload.link_mode == :on_demand }]
|
|
34
|
+
@on_demand_links.last
|
|
35
|
+
end
|
|
36
|
+
|
|
29
37
|
def self.inherited(klass)
|
|
30
38
|
super
|
|
31
39
|
klass.class_eval do
|
|
@@ -76,7 +84,7 @@ module Graphiti
|
|
|
76
84
|
|
|
77
85
|
def as_jsonapi(kwargs = {})
|
|
78
86
|
super(**kwargs).tap do |hash|
|
|
79
|
-
strip_relationships!(hash)
|
|
87
|
+
strip_relationships!(hash)
|
|
80
88
|
add_links!(hash)
|
|
81
89
|
end
|
|
82
90
|
end
|
|
@@ -102,21 +110,29 @@ module Graphiti
|
|
|
102
110
|
hash[:links] = @resource.links(@object) if @resource.links?
|
|
103
111
|
end
|
|
104
112
|
|
|
105
|
-
#
|
|
106
|
-
# serializes as an empty stub, which JSON:API forbids.
|
|
113
|
+
# The meta: {included: false} stub is jsonapi-serializable's filler, not JSON:API.
|
|
107
114
|
def strip_relationships!(hash)
|
|
115
|
+
placeholders = relationship_placeholders?
|
|
116
|
+
return if placeholders && !strip_on_demand_relationships?
|
|
117
|
+
|
|
108
118
|
hash[:relationships]&.reject! do |name, payload|
|
|
109
119
|
next false if payload.key?(:data) || payload.key?(:links)
|
|
120
|
+
next true unless placeholders
|
|
110
121
|
|
|
111
122
|
self.class.relationship_sideloads[name]&.link_mode == :on_demand
|
|
112
123
|
end
|
|
113
124
|
end
|
|
114
125
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
126
|
+
# A remote resource exposes a stand-in object rather than a Resource.
|
|
127
|
+
def relationship_placeholders?
|
|
128
|
+
resource_class = @resource.class
|
|
129
|
+
return Resource.relationship_placeholders unless resource_class.respond_to?(:relationship_placeholders)
|
|
130
|
+
|
|
131
|
+
resource_class.relationship_placeholders
|
|
132
|
+
end
|
|
118
133
|
|
|
119
|
-
|
|
134
|
+
def strip_on_demand_relationships?
|
|
135
|
+
self.class.on_demand_links? && !@proxy&.query&.render_link?(:on_demand)
|
|
120
136
|
end
|
|
121
137
|
end
|
|
122
138
|
end
|
|
@@ -107,18 +107,18 @@ class Graphiti::Sideload::PolymorphicBelongsTo < Graphiti::Sideload::BelongsTo
|
|
|
107
107
|
end
|
|
108
108
|
end
|
|
109
109
|
|
|
110
|
-
def resolve(parents, query, graph_parent)
|
|
110
|
+
def resolve(parents, query, graph_parent, &proxy_block)
|
|
111
111
|
if ::Graphiti::Scope.resolve_synchronously?
|
|
112
|
-
sync_resolve(parents, query, graph_parent)
|
|
112
|
+
sync_resolve(parents, query, graph_parent, &proxy_block)
|
|
113
113
|
else
|
|
114
|
-
future_resolve(parents, query, graph_parent).value!
|
|
114
|
+
future_resolve(parents, query, graph_parent, &proxy_block).value!
|
|
115
115
|
end
|
|
116
116
|
end
|
|
117
117
|
|
|
118
|
-
def future_resolve(parents, query, graph_parent)
|
|
118
|
+
def future_resolve(parents, query, graph_parent, &proxy_block)
|
|
119
119
|
promises = []
|
|
120
120
|
each_resolvable_group(parents, query) do |child, group, child_query|
|
|
121
|
-
promises << child.future_resolve(group, child_query, graph_parent)
|
|
121
|
+
promises << child.future_resolve(group, child_query, graph_parent, &proxy_block)
|
|
122
122
|
end
|
|
123
123
|
return promises.first if promises.one?
|
|
124
124
|
|
|
@@ -130,14 +130,14 @@ class Graphiti::Sideload::PolymorphicBelongsTo < Graphiti::Sideload::BelongsTo
|
|
|
130
130
|
end
|
|
131
131
|
end
|
|
132
132
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def sync_resolve(parents, query, graph_parent)
|
|
133
|
+
def sync_resolve(parents, query, graph_parent, &proxy_block)
|
|
136
134
|
each_resolvable_group(parents, query) do |child, group, child_query|
|
|
137
|
-
child.
|
|
135
|
+
child.sync_resolve(group, child_query, graph_parent, &proxy_block)
|
|
138
136
|
end
|
|
139
137
|
end
|
|
140
138
|
|
|
139
|
+
private
|
|
140
|
+
|
|
141
141
|
# Group parents by their polymorphic type and yield each group's child
|
|
142
142
|
# sideload alongside a query pruned to the sideloads that child supports.
|
|
143
143
|
def each_resolvable_group(parents, query)
|
data/lib/graphiti/sideload.rb
CHANGED
|
@@ -152,8 +152,12 @@ module Graphiti
|
|
|
152
152
|
!!@polymorphic_as
|
|
153
153
|
end
|
|
154
154
|
|
|
155
|
+
# Every check behind the blocker is static sideload configuration, and this
|
|
156
|
+
# is asked once per rendered record per relationship.
|
|
155
157
|
def resource_ids_from_foreign_key?
|
|
156
|
-
|
|
158
|
+
return @resource_ids_from_foreign_key unless @resource_ids_from_foreign_key.nil?
|
|
159
|
+
|
|
160
|
+
@resource_ids_from_foreign_key = resource_ids_blocker.nil?
|
|
157
161
|
end
|
|
158
162
|
|
|
159
163
|
def resource_ids_blocker
|
|
@@ -285,14 +289,20 @@ module Graphiti
|
|
|
285
289
|
proxy
|
|
286
290
|
end
|
|
287
291
|
|
|
288
|
-
def load(parents, query, graph_parent)
|
|
292
|
+
def load(parents, query, graph_parent, &proxy_block)
|
|
289
293
|
if Scope.resolve_synchronously?
|
|
290
|
-
|
|
294
|
+
sync_load(parents, query, graph_parent, &proxy_block)
|
|
291
295
|
else
|
|
292
|
-
future_load(parents, query, graph_parent).value!
|
|
296
|
+
future_load(parents, query, graph_parent, &proxy_block).value!
|
|
293
297
|
end
|
|
294
298
|
end
|
|
295
299
|
|
|
300
|
+
def sync_load(parents, query, graph_parent, &proxy_block)
|
|
301
|
+
proxy = build_resource_proxy(parents, query, graph_parent)
|
|
302
|
+
proxy_block&.call(proxy)
|
|
303
|
+
proxy.to_a
|
|
304
|
+
end
|
|
305
|
+
|
|
296
306
|
# Override in subclass
|
|
297
307
|
def infer_foreign_key
|
|
298
308
|
model = parent_resource_class.model
|
|
@@ -340,15 +350,30 @@ module Graphiti
|
|
|
340
350
|
children.replace(associated) if track_associated
|
|
341
351
|
end
|
|
342
352
|
|
|
343
|
-
def resolve(parents, query, graph_parent)
|
|
353
|
+
def resolve(parents, query, graph_parent, &proxy_block)
|
|
344
354
|
if Scope.resolve_synchronously?
|
|
345
|
-
sync_resolve(parents, query, graph_parent)
|
|
355
|
+
sync_resolve(parents, query, graph_parent, &proxy_block)
|
|
356
|
+
else
|
|
357
|
+
future_resolve(parents, query, graph_parent, &proxy_block).value!
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# Called by a scope that already decided to stay inline, so it must not consult the pool again.
|
|
362
|
+
# A scope_proc builds a Scope rather than a proxy, and that nested scope decides for itself.
|
|
363
|
+
def sync_resolve(parents, query, graph_parent, &proxy_block)
|
|
364
|
+
assert_singular!(parents)
|
|
365
|
+
|
|
366
|
+
if self.class.scope_proc
|
|
367
|
+
build_sideload_scope(parents, query, graph_parent).resolve do |sideload_results|
|
|
368
|
+
fire_assign(parents, sideload_results)
|
|
369
|
+
end
|
|
346
370
|
else
|
|
347
|
-
|
|
371
|
+
sync_load(parents, query, graph_parent, &proxy_block)
|
|
348
372
|
end
|
|
349
373
|
end
|
|
350
374
|
|
|
351
|
-
|
|
375
|
+
# A scope_proc builds a Scope rather than a proxy, and a Scope's cache key omits what a proxy's carries.
|
|
376
|
+
def future_resolve(parents, query, graph_parent, &proxy_block)
|
|
352
377
|
assert_singular!(parents)
|
|
353
378
|
|
|
354
379
|
if self.class.scope_proc
|
|
@@ -356,7 +381,7 @@ module Graphiti
|
|
|
356
381
|
fire_assign(parents, sideload_results)
|
|
357
382
|
end
|
|
358
383
|
else
|
|
359
|
-
future_load(parents, query, graph_parent)
|
|
384
|
+
future_load(parents, query, graph_parent, &proxy_block)
|
|
360
385
|
end
|
|
361
386
|
end
|
|
362
387
|
|
|
@@ -420,21 +445,6 @@ module Graphiti
|
|
|
420
445
|
|
|
421
446
|
private
|
|
422
447
|
|
|
423
|
-
# Synchronous counterpart to #future_resolve, used when concurrency is off.
|
|
424
|
-
# Resolves inline via the synchronous Scope#resolve / #load paths (no
|
|
425
|
-
# promises). See Scope#sync_resolve_sideloads.
|
|
426
|
-
def sync_resolve(parents, query, graph_parent)
|
|
427
|
-
assert_singular!(parents)
|
|
428
|
-
|
|
429
|
-
if self.class.scope_proc
|
|
430
|
-
build_sideload_scope(parents, query, graph_parent).resolve do |sideload_results|
|
|
431
|
-
fire_assign(parents, sideload_results)
|
|
432
|
-
end
|
|
433
|
-
else
|
|
434
|
-
load(parents, query, graph_parent)
|
|
435
|
-
end
|
|
436
|
-
end
|
|
437
|
-
|
|
438
448
|
def assert_singular!(parents)
|
|
439
449
|
if single? && parents.length > 1
|
|
440
450
|
raise Errors::SingularSideload.new(self, parents.length)
|
|
@@ -451,8 +461,9 @@ module Graphiti
|
|
|
451
461
|
default_paginate: false
|
|
452
462
|
end
|
|
453
463
|
|
|
454
|
-
def future_load(parents, query, graph_parent)
|
|
464
|
+
def future_load(parents, query, graph_parent, &proxy_block)
|
|
455
465
|
proxy = build_resource_proxy(parents, query, graph_parent)
|
|
466
|
+
proxy_block&.call(proxy)
|
|
456
467
|
proxy.respond_to?(:future_resolve_data) ? proxy.future_resolve_data : Concurrent::Promises.fulfilled_future(proxy)
|
|
457
468
|
end
|
|
458
469
|
|
|
@@ -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(blanks: :
|
|
189
|
+
# @example expect(subject).to filter_attribute(:name, :string).with_options(blanks: :null)
|
|
190
190
|
# @example expect(subject).not_to filter_attribute(:name, :string)
|
|
191
191
|
def filter_attribute(attribute, type)
|
|
192
192
|
FilterAttributeMatcher.new(attribute, type)
|
data/lib/graphiti/version.rb
CHANGED
data/package.json
CHANGED
|
@@ -142,7 +142,7 @@
|
|
|
142
142
|
[
|
|
143
143
|
"@semantic-release/exec",
|
|
144
144
|
{
|
|
145
|
-
"prepareCmd": "bundle exec ruby spec/performance/measure_releases.rb --
|
|
145
|
+
"prepareCmd": "bundle exec ruby spec/performance/measure_releases.rb --promote v${nextRelease.version}"
|
|
146
146
|
}
|
|
147
147
|
],
|
|
148
148
|
[
|