graphiti 2.0.0.beta.9 → 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 +46 -0
- data/Rakefile +17 -12
- data/lib/generators/graphiti/generator_mixin.rb +24 -0
- data/lib/generators/graphiti/templates/application_resource.rb.erb +9 -16
- data/lib/graphiti/adapters/abstract.rb +4 -3
- data/lib/graphiti/configuration.rb +55 -24
- data/lib/graphiti/delegates/pagination.rb +2 -2
- data/lib/graphiti/errors.rb +35 -3
- data/lib/graphiti/query.rb +61 -13
- data/lib/graphiti/rails/controller.rb +26 -6
- data/lib/graphiti/rails/rake_helpers.rb +17 -0
- data/lib/graphiti/renderer.rb +4 -1
- data/lib/graphiti/request_validators/update_validator.rb +1 -1
- data/lib/graphiti/request_validators/validator.rb +9 -0
- data/lib/graphiti/resource/configuration.rb +139 -40
- data/lib/graphiti/resource/dsl.rb +21 -2
- data/lib/graphiti/resource/interface.rb +1 -1
- data/lib/graphiti/resource/links.rb +39 -5
- data/lib/graphiti/resource/remote.rb +2 -1
- data/lib/graphiti/resource/sideloading.rb +2 -2
- data/lib/graphiti/schema.rb +2 -2
- data/lib/graphiti/scope.rb +100 -26
- data/lib/graphiti/scoping/filter.rb +4 -4
- data/lib/graphiti/scoping/paginate.rb +3 -3
- data/lib/graphiti/serializer.rb +30 -8
- data/lib/graphiti/sideload/polymorphic_belongs_to.rb +9 -9
- data/lib/graphiti/sideload.rb +50 -33
- data/lib/graphiti/spec_helpers/matchers.rb +2 -2
- data/lib/graphiti/spec_helpers/rspec.rb +6 -3
- data/lib/graphiti/util/link.rb +1 -1
- data/lib/graphiti/util/serializer_attributes.rb +5 -3
- data/lib/graphiti/util/serializer_relationships.rb +16 -15
- data/lib/graphiti/version.rb +1 -1
- data/lib/graphiti.rb +5 -0
- data/lib/tasks/graphiti.rake +4 -0
- 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,51 @@
|
|
|
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
|
+
|
|
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)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
### Bug Fixes
|
|
31
|
+
|
|
32
|
+
* apply sideloads at definition once setup! has run ([39ef393](https://github.com/graphiti-api/graphiti/commit/39ef39383520816319282854c0ae99677bcd5127)), closes [#524](https://github.com/graphiti-api/graphiti/issues/524)
|
|
33
|
+
* give client-caused errors real detail and stop 500ing on bad query params ([35acc3e](https://github.com/graphiti-api/graphiti/commit/35acc3e6c78f68eeeb05728ea16264b136455c30)), closes [#531](https://github.com/graphiti-api/graphiti/issues/531)
|
|
34
|
+
* let an abstract resource declare relationships ([6036392](https://github.com/graphiti-api/graphiti/commit/6036392ebd611c7ed492beb3fe5754433eac9ffd)), closes [#453](https://github.com/graphiti-api/graphiti/issues/453)
|
|
35
|
+
* reject a non-object data payload instead of raising TypeError ([51603b8](https://github.com/graphiti-api/graphiti/commit/51603b854270715b80a75d9aa05a4020e6bbdce5))
|
|
36
|
+
* Update some missing sections and terms after renames, register unsupported pagination error ([39cff19](https://github.com/graphiti-api/graphiti/commit/39cff19f6f1f73126514c8b18234a18ad5eb7379))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
### Features
|
|
40
|
+
|
|
41
|
+
* carry CurrentAttributes into concurrent sideloads ([7092096](https://github.com/graphiti-api/graphiti/commit/7092096f6d99ad277cc56817c616d946f655d55b))
|
|
42
|
+
* fold allow_nil and deny_empty into one blanks: filter option ([a108403](https://github.com/graphiti-api/graphiti/commit/a108403e5cd87fd2fdf177da30c4c4f97b9fe750))
|
|
43
|
+
* move link rendering onto the resource as page_links and relationship_links ([57878d1](https://github.com/graphiti-api/graphiti/commit/57878d14fdfc4741e8d418c8edbc4a8687335843))
|
|
44
|
+
* move typecast_reads onto the resource and let the railtie own Rails config ([236e970](https://github.com/graphiti-api/graphiti/commit/236e9706d7d8f08e9640c5c22ac596e2f7466849))
|
|
45
|
+
* one link mode per relationship, cascading from relationship_links ([f0e1da1](https://github.com/graphiti-api/graphiti/commit/f0e1da1e13a8d12b47458495b378779f5687ffe2))
|
|
46
|
+
* point connection pool timeouts at the sizing formula and add an audit task to check ([6e017c6](https://github.com/graphiti-api/graphiti/commit/6e017c60fd45338e4e6c8527adebfd46be4bfd8d))
|
|
47
|
+
* split validate_endpoints into validate_requests and validate_links ([100a370](https://github.com/graphiti-api/graphiti/commit/100a370deec6a0e6ae165bdf77e50b6b14d068d3))
|
|
48
|
+
|
|
3
49
|
# [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
50
|
|
|
5
51
|
|
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
|
|
|
@@ -33,6 +33,30 @@ module Graphiti
|
|
|
33
33
|
methods.any? { |m| actions.include?(m) }
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
+
def resource_setting_groups
|
|
37
|
+
name_width = Graphiti::Resource::SETTINGS.keys.map(&:length).max
|
|
38
|
+
assignments = Graphiti::Resource::SETTINGS.to_h do |name, setting|
|
|
39
|
+
value = setting[:format] || setting[:default].inspect
|
|
40
|
+
[name, "self.#{name.to_s.ljust(name_width)} = #{value}"]
|
|
41
|
+
end
|
|
42
|
+
hint_column = assignments.values.map(&:length).max + 2
|
|
43
|
+
|
|
44
|
+
Graphiti::Resource::SETTING_GROUPS.transform_values do |settings|
|
|
45
|
+
settings.map do |name, setting|
|
|
46
|
+
hint = resource_setting_hint(setting)
|
|
47
|
+
hint ? "#{assignments[name].ljust(hint_column)}# #{hint}" : assignments[name]
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def resource_setting_hint(setting)
|
|
53
|
+
return setting[:note] if setting[:note]
|
|
54
|
+
return unless setting[:values]
|
|
55
|
+
|
|
56
|
+
setting[:values].map(&:inspect)
|
|
57
|
+
.to_sentence(two_words_connector: " or ", last_word_connector: ", or ")
|
|
58
|
+
end
|
|
59
|
+
|
|
36
60
|
def graphiti_config
|
|
37
61
|
File.exist?(".graphiticfg.yml") ? YAML.load_file(".graphiticfg.yml") : {}
|
|
38
62
|
end
|
|
@@ -14,21 +14,14 @@ class ApplicationResource < Graphiti::Resource
|
|
|
14
14
|
self.endpoint_namespace = '<%= api_namespace %>'
|
|
15
15
|
<%- unless omit_comments? -%>
|
|
16
16
|
|
|
17
|
-
# Defaults every Resource inherits.
|
|
18
|
-
#
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
#
|
|
22
|
-
|
|
23
|
-
#
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
# self.relationships_readable_by_default = true
|
|
27
|
-
# self.relationships_writable_by_default = true
|
|
28
|
-
# self.belongs_to_resource_ids_by_default = :foreign_key
|
|
29
|
-
# self.filters_accept_nil_by_default = false
|
|
30
|
-
# self.filters_deny_empty_by_default = false
|
|
31
|
-
# self.default_page_size = 20
|
|
32
|
-
# self.max_page_size = 1_000
|
|
17
|
+
# Defaults every Resource inherits. Uncomment to change them API-wide, or set
|
|
18
|
+
# them on individual Resources. See https://graphiti.dev/concepts/resources
|
|
19
|
+
<%- resource_setting_groups.each_pair do |group, lines| -%>
|
|
20
|
+
|
|
21
|
+
# <%= group.to_s.humanize %>
|
|
22
|
+
<%- lines.each do |line| -%>
|
|
23
|
+
# <%= line %>
|
|
24
|
+
<%- end -%>
|
|
25
|
+
<%- end -%>
|
|
33
26
|
<%- end -%>
|
|
34
27
|
end
|
|
@@ -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)
|
|
@@ -24,10 +24,6 @@ module Graphiti
|
|
|
24
24
|
|
|
25
25
|
attr_accessor :respond_to
|
|
26
26
|
attr_accessor :context_for_endpoint
|
|
27
|
-
attr_accessor :links_on_demand
|
|
28
|
-
attr_accessor :pagination_links_on_demand
|
|
29
|
-
attr_accessor :pagination_links
|
|
30
|
-
attr_accessor :typecast_reads
|
|
31
27
|
attr_accessor :raise_on_missing_sidepost
|
|
32
28
|
attr_accessor :before_sideload
|
|
33
29
|
|
|
@@ -44,32 +40,12 @@ module Graphiti
|
|
|
44
40
|
@concurrency = false
|
|
45
41
|
@concurrency_max_threads = 4
|
|
46
42
|
@respond_to = [:json, :jsonapi, :xml]
|
|
47
|
-
@links_on_demand = false
|
|
48
|
-
@pagination_links_on_demand = false
|
|
49
|
-
@pagination_links = false
|
|
50
|
-
@typecast_reads = true
|
|
51
43
|
@raise_on_missing_sidepost = true
|
|
52
44
|
@cache_rendering = false
|
|
53
45
|
self.debug = ENV.fetch("GRAPHITI_DEBUG", true)
|
|
54
46
|
self.debug_models = ENV.fetch("GRAPHITI_DEBUG_MODELS", false)
|
|
55
47
|
|
|
56
48
|
@uri_decoder = infer_uri_decoder
|
|
57
|
-
|
|
58
|
-
# FIXME: Don't duplicate the Railtie's initializer
|
|
59
|
-
if defined?(::Rails.root) && (root = ::Rails.root)
|
|
60
|
-
config_file = root.join(".graphiticfg.yml")
|
|
61
|
-
if config_file.exist?
|
|
62
|
-
cfg = YAML.load_file(config_file)
|
|
63
|
-
@schema_path = root.join("public#{cfg["namespace"]}/schema.json")
|
|
64
|
-
else
|
|
65
|
-
@schema_path = root.join("public/schema.json")
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
if (logger = ::Rails.logger)
|
|
69
|
-
self.debug = logger.debug? && debug
|
|
70
|
-
Graphiti.logger = logger
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
49
|
end
|
|
74
50
|
|
|
75
51
|
def cache_rendering?
|
|
@@ -111,6 +87,48 @@ module Graphiti
|
|
|
111
87
|
send(:"#{key}=", original)
|
|
112
88
|
end
|
|
113
89
|
|
|
90
|
+
def typecast_reads
|
|
91
|
+
Resource.typecast_reads
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def typecast_reads=(val)
|
|
95
|
+
Resource.typecast_reads = val
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def links_on_demand
|
|
99
|
+
Resource.relationship_links == :on_demand
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def links_on_demand=(val)
|
|
103
|
+
if val
|
|
104
|
+
Resource.relationship_links = :on_demand
|
|
105
|
+
elsif Resource.relationship_links == :on_demand
|
|
106
|
+
Resource.relationship_links = true
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def pagination_links
|
|
111
|
+
Resource.page_links == true
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def pagination_links=(val)
|
|
115
|
+
return if Resource.page_links == :on_demand
|
|
116
|
+
|
|
117
|
+
Resource.page_links = !!val
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def pagination_links_on_demand
|
|
121
|
+
Resource.page_links == :on_demand
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def pagination_links_on_demand=(val)
|
|
125
|
+
if val
|
|
126
|
+
Resource.page_links = :on_demand
|
|
127
|
+
elsif Resource.page_links == :on_demand
|
|
128
|
+
Resource.page_links = false
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
114
132
|
def uri_decoder=(decoder)
|
|
115
133
|
unless decoder.respond_to?(:call)
|
|
116
134
|
raise "uri_decoder must respond to `call`."
|
|
@@ -141,4 +159,17 @@ module Graphiti
|
|
|
141
159
|
|
|
142
160
|
msg = "Use `config.graphiti.respond_to_formats`"
|
|
143
161
|
DEPRECATOR.deprecate_methods(Configuration, respond_to: msg, "respond_to=": msg)
|
|
162
|
+
|
|
163
|
+
relationship_msg = "Set `self.relationship_links` (true, false, or :on_demand) on your resource"
|
|
164
|
+
pagination_msg = "Set `self.page_links` (true, false, or :on_demand) on your resource"
|
|
165
|
+
typecast_msg = "Set `self.typecast_reads` on your resource"
|
|
166
|
+
DEPRECATOR.deprecate_methods(Configuration,
|
|
167
|
+
typecast_reads: typecast_msg,
|
|
168
|
+
"typecast_reads=": typecast_msg,
|
|
169
|
+
links_on_demand: relationship_msg,
|
|
170
|
+
"links_on_demand=": relationship_msg,
|
|
171
|
+
pagination_links: pagination_msg,
|
|
172
|
+
"pagination_links=": pagination_msg,
|
|
173
|
+
pagination_links_on_demand: pagination_msg,
|
|
174
|
+
"pagination_links_on_demand=": pagination_msg)
|
|
144
175
|
end
|
|
@@ -6,7 +6,7 @@ module Graphiti
|
|
|
6
6
|
end
|
|
7
7
|
|
|
8
8
|
def links?
|
|
9
|
-
@proxy.query.
|
|
9
|
+
@proxy.query.page_links? && @proxy.data.present?
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def links
|
|
@@ -104,7 +104,7 @@ module Graphiti
|
|
|
104
104
|
|
|
105
105
|
def page_size
|
|
106
106
|
@page_size ||= (page_param[:size] ||
|
|
107
|
-
@proxy.resource.
|
|
107
|
+
@proxy.resource.page_default_size ||
|
|
108
108
|
Graphiti::Scoping::Paginate::DEFAULT_PAGE_SIZE).to_i
|
|
109
109
|
end
|
|
110
110
|
|
data/lib/graphiti/errors.rb
CHANGED
|
@@ -221,7 +221,7 @@ module Graphiti
|
|
|
221
221
|
|
|
222
222
|
Make sure the endpoint "#{@sideload.resource.endpoint[:full_path]}" exists with action #{@action.inspect}, or customize the endpoint for #{@sideload.resource.class.name}.
|
|
223
223
|
|
|
224
|
-
If you do not wish to generate a link, pass link: false or set self.
|
|
224
|
+
If you do not wish to generate a link, pass link: false or set self.relationship_links = false. To keep the link and stop checking it, set self.validate_links = false.
|
|
225
225
|
MSG
|
|
226
226
|
end
|
|
227
227
|
end
|
|
@@ -255,6 +255,8 @@ module Graphiti
|
|
|
255
255
|
Graphiti.config.context_for_endpoint must be set to enable link generation:
|
|
256
256
|
|
|
257
257
|
Graphiti.config.context_for_endpoint = ->(path, action) { ... }
|
|
258
|
+
|
|
259
|
+
Or set self.validate_links = false to render links without checking them.
|
|
258
260
|
MSG
|
|
259
261
|
end
|
|
260
262
|
end
|
|
@@ -445,9 +447,9 @@ module Graphiti
|
|
|
445
447
|
|
|
446
448
|
secondary_endpoint '/my_url', [:index, :update]
|
|
447
449
|
|
|
448
|
-
Or disable
|
|
450
|
+
Or disable request validation for this resource:
|
|
449
451
|
|
|
450
|
-
self.
|
|
452
|
+
self.validate_requests = false
|
|
451
453
|
|
|
452
454
|
See https://graphiti.dev/concepts/links for more information.
|
|
453
455
|
|
|
@@ -708,6 +710,36 @@ module Graphiti
|
|
|
708
710
|
end
|
|
709
711
|
end
|
|
710
712
|
|
|
713
|
+
class InvalidFilterBlanks < Base
|
|
714
|
+
def initialize(resource_class, attribute, value)
|
|
715
|
+
@resource_class = resource_class
|
|
716
|
+
@attribute = attribute
|
|
717
|
+
@value = value
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
def message
|
|
721
|
+
<<~MSG
|
|
722
|
+
#{@resource_class.name}: #{@attribute} must be one of :literal, :null, or :rejected. Got #{@value.inspect}.
|
|
723
|
+
|
|
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
|
+
MSG
|
|
728
|
+
end
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
class InvalidLinkRendering < Base
|
|
732
|
+
def initialize(resource_class, attribute, value)
|
|
733
|
+
@resource_class = resource_class
|
|
734
|
+
@attribute = attribute
|
|
735
|
+
@value = value
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
def message
|
|
739
|
+
"#{@resource_class.name}: #{@attribute} must be true, false, or :on_demand. Got #{@value.inspect}."
|
|
740
|
+
end
|
|
741
|
+
end
|
|
742
|
+
|
|
711
743
|
class UnselectedForeignKey < Base
|
|
712
744
|
def initialize(resource_class, sideload, model)
|
|
713
745
|
@resource_class = resource_class
|
data/lib/graphiti/query.rb
CHANGED
|
@@ -2,6 +2,9 @@ require "digest"
|
|
|
2
2
|
|
|
3
3
|
module Graphiti
|
|
4
4
|
class Query
|
|
5
|
+
TRUTHY_LINKS = [true, "true"].freeze
|
|
6
|
+
LINKLESS_FORMATS = [:json, :xml, "json", "xml"].freeze
|
|
7
|
+
|
|
5
8
|
attr_reader :resource, :association_name, :params, :action
|
|
6
9
|
|
|
7
10
|
def initialize(resource, params, *positional, association_name: nil, nested_include: nil, parents: [], action: nil)
|
|
@@ -36,29 +39,70 @@ module Graphiti
|
|
|
36
39
|
@entity_map
|
|
37
40
|
end
|
|
38
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
|
+
|
|
39
65
|
def top_level?
|
|
40
66
|
!association?
|
|
41
67
|
end
|
|
42
68
|
|
|
43
|
-
def
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
69
|
+
def includes_requested?
|
|
70
|
+
@include_param.present?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def links_requested?
|
|
74
|
+
return @links_requested if defined?(@links_requested)
|
|
75
|
+
|
|
76
|
+
@links_requested = TRUTHY_LINKS.include?(@params[:links])
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def suppress_links?
|
|
80
|
+
return @suppress_links if defined?(@suppress_links)
|
|
81
|
+
|
|
82
|
+
@suppress_links = LINKLESS_FORMATS.include?(params[:format])
|
|
50
83
|
end
|
|
51
84
|
|
|
52
|
-
def
|
|
85
|
+
def render_link?(mode)
|
|
86
|
+
return false if suppress_links?
|
|
87
|
+
|
|
88
|
+
(mode == :on_demand) ? links_requested? : mode == true
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def page_links?
|
|
53
92
|
if action == :find
|
|
54
93
|
false
|
|
55
|
-
elsif
|
|
56
|
-
|
|
94
|
+
elsif @resource.page_links == :on_demand
|
|
95
|
+
page_links_requested?
|
|
57
96
|
else
|
|
58
|
-
|
|
97
|
+
@resource.page_links
|
|
59
98
|
end
|
|
60
99
|
end
|
|
61
100
|
|
|
101
|
+
def page_links_requested?
|
|
102
|
+
[@params[:page_links], @params[:pagination_links]]
|
|
103
|
+
.any? { |value| [true, "true"].include?(value) }
|
|
104
|
+
end
|
|
105
|
+
|
|
62
106
|
def debug_requested?
|
|
63
107
|
!!@params[:debug]
|
|
64
108
|
end
|
|
@@ -267,8 +311,8 @@ module Graphiti
|
|
|
267
311
|
def query_cache_key
|
|
268
312
|
attrs = {extra_fields: extra_fields,
|
|
269
313
|
fields: fields,
|
|
270
|
-
links:
|
|
271
|
-
|
|
314
|
+
links: suppress_links? ? :none : links_requested?,
|
|
315
|
+
page_links: page_links?,
|
|
272
316
|
format: params[:format]}
|
|
273
317
|
|
|
274
318
|
Digest::SHA1.hexdigest(attrs.to_s)
|
|
@@ -387,5 +431,9 @@ module Graphiti
|
|
|
387
431
|
action
|
|
388
432
|
end
|
|
389
433
|
end
|
|
434
|
+
|
|
435
|
+
alias_method :pagination_links?, :page_links?
|
|
436
|
+
DEPRECATOR.deprecate_methods(self,
|
|
437
|
+
pagination_links?: "Use `page_links?`")
|
|
390
438
|
end
|
|
391
439
|
end
|
|
@@ -30,14 +30,34 @@ module Graphiti
|
|
|
30
30
|
|
|
31
31
|
register_exception Graphiti::Errors::InvalidRequest,
|
|
32
32
|
status: 400, handler: Graphiti::Rails::InvalidRequestHandler
|
|
33
|
-
register_exception Graphiti::Errors::ConflictRequest,
|
|
34
|
-
status: 409, handler: Graphiti::Rails::ConflictRequestHandler
|
|
35
|
-
register_exception Graphiti::Errors::RecordNotFound,
|
|
36
|
-
status: 404, handler: Graphiti::Rails::ExceptionHandler
|
|
37
33
|
register_exception Graphiti::Errors::RemoteWrite,
|
|
38
|
-
status: 400, handler: Graphiti::Rails::ExceptionHandler
|
|
34
|
+
status: 400, detail: :exception, handler: Graphiti::Rails::ExceptionHandler
|
|
39
35
|
register_exception Graphiti::Errors::SingularSideload,
|
|
40
|
-
status: 400, handler: Graphiti::Rails::ExceptionHandler
|
|
36
|
+
status: 400, detail: :exception, handler: Graphiti::Rails::ExceptionHandler
|
|
37
|
+
register_exception Graphiti::Errors::InvalidInclude,
|
|
38
|
+
status: 400, detail: :exception, handler: Graphiti::Rails::ExceptionHandler
|
|
39
|
+
register_exception Graphiti::Errors::UnsupportedSort,
|
|
40
|
+
status: 400, detail: :exception, handler: Graphiti::Rails::ExceptionHandler
|
|
41
|
+
register_exception Graphiti::Errors::UnsupportedOperator,
|
|
42
|
+
status: 400, detail: :exception, handler: Graphiti::Rails::ExceptionHandler
|
|
43
|
+
register_exception Graphiti::Errors::InvalidFilterValue,
|
|
44
|
+
status: 400, detail: :exception, handler: Graphiti::Rails::ExceptionHandler
|
|
45
|
+
register_exception Graphiti::Errors::RequiredFilter,
|
|
46
|
+
status: 400, detail: :exception, handler: Graphiti::Rails::ExceptionHandler
|
|
47
|
+
register_exception Graphiti::Errors::UnsupportedPageSize,
|
|
48
|
+
status: 400, detail: :exception, handler: Graphiti::Rails::ExceptionHandler
|
|
49
|
+
register_exception Graphiti::Errors::UnsupportedBeforeCursor,
|
|
50
|
+
status: 400, detail: :exception, handler: Graphiti::Rails::ExceptionHandler
|
|
51
|
+
register_exception Graphiti::Errors::UnsupportedPagination,
|
|
52
|
+
status: 400, detail: :exception, handler: Graphiti::Rails::ExceptionHandler
|
|
53
|
+
register_exception Graphiti::Errors::FilterGroupMissingRequiredFilters,
|
|
54
|
+
status: 400, detail: :exception, handler: Graphiti::Rails::ExceptionHandler
|
|
55
|
+
|
|
56
|
+
register_exception Graphiti::Errors::RecordNotFound,
|
|
57
|
+
status: 404, detail: :exception, handler: Graphiti::Rails::ExceptionHandler
|
|
58
|
+
|
|
59
|
+
register_exception Graphiti::Errors::ConflictRequest,
|
|
60
|
+
status: 409, handler: Graphiti::Rails::ConflictRequestHandler
|
|
41
61
|
end
|
|
42
62
|
end
|
|
43
63
|
end
|
|
@@ -21,6 +21,23 @@ module Graphiti
|
|
|
21
21
|
::Rails.application.config.action_controller.perform_caching = false
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
+
# The pool and RAILS_MAX_THREADS are read from this environment, and production may size both differently.
|
|
25
|
+
def connection_pool_advisory
|
|
26
|
+
return unless defined?(ActiveRecord::Base)
|
|
27
|
+
|
|
28
|
+
pool = ActiveRecord::Base.connection_db_config.pool
|
|
29
|
+
threads = ENV.fetch("RAILS_MAX_THREADS", 5).to_i
|
|
30
|
+
sideload_threads = Graphiti.config.concurrency_max_threads
|
|
31
|
+
needed = threads + sideload_threads + 1
|
|
32
|
+
return if pool.nil? || pool >= needed
|
|
33
|
+
|
|
34
|
+
"WARNING database.yml pool is #{pool} in this environment. With concurrency on (the production default), " \
|
|
35
|
+
"each sideload holds its own connection, so pool should be at least " \
|
|
36
|
+
"web threads (#{threads}) + concurrency_max_threads (#{sideload_threads}) + 1 = #{needed}. " \
|
|
37
|
+
"If production sizes these differently, check the numbers there. " \
|
|
38
|
+
"See graphiti.dev/concepts/resources#concurrency-pool-sizing."
|
|
39
|
+
end
|
|
40
|
+
|
|
24
41
|
def make_request(path, debug = false)
|
|
25
42
|
if path.split("/").length == 2
|
|
26
43
|
path = "#{ApplicationResource.endpoint_namespace}#{path}"
|
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
|
|
@@ -17,6 +17,7 @@ module Graphiti
|
|
|
17
17
|
# In the future these should have their own validation logic, but
|
|
18
18
|
# for now we can just bypass
|
|
19
19
|
return errors.blank? unless @params.has_key?(:data)
|
|
20
|
+
return false unless validate_data_shape
|
|
20
21
|
|
|
21
22
|
resource = @root_resource
|
|
22
23
|
if @params[:data].has_key?(:type)
|
|
@@ -56,6 +57,14 @@ module Graphiti
|
|
|
56
57
|
@errors.add(:page, :invalid, message: "must be an object")
|
|
57
58
|
end
|
|
58
59
|
|
|
60
|
+
def validate_data_shape
|
|
61
|
+
data = @params[:data]
|
|
62
|
+
return true if data.nil? || data.respond_to?(:each_pair)
|
|
63
|
+
|
|
64
|
+
@errors.add(:data, :invalid, message: "must be an object")
|
|
65
|
+
false
|
|
66
|
+
end
|
|
67
|
+
|
|
59
68
|
def process_relationships(resource, relationships, payload_path)
|
|
60
69
|
relationships.each_key do |name|
|
|
61
70
|
unless resource.class.sideload(name.to_sym)
|