graphiti 2.0.0.beta.9 → 2.0.0.beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -0
- data/lib/generators/graphiti/generator_mixin.rb +24 -0
- data/lib/generators/graphiti/templates/application_resource.rb.erb +9 -16
- 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 +34 -13
- data/lib/graphiti/rails/controller.rb +26 -6
- data/lib/graphiti/rails/rake_helpers.rb +17 -0
- 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 +138 -40
- data/lib/graphiti/resource/dsl.rb +21 -2
- data/lib/graphiti/resource/interface.rb +1 -1
- data/lib/graphiti/resource/links.rb +36 -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 +49 -4
- data/lib/graphiti/scoping/filter.rb +4 -4
- data/lib/graphiti/scoping/paginate.rb +3 -3
- data/lib/graphiti/serializer.rb +12 -6
- data/lib/graphiti/sideload.rb +14 -8
- 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
- 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: b1e331ab2dea47987ea47260eb0890c9a953d3435bba2e7ef37cbfbbc83fe5ab
|
|
4
|
+
data.tar.gz: 13a7483ead9f4ad5ed69390ff6a4a122a819813c2147c0a56dceebdfebc57e17
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cca252389b52b5a2983407b4eff7cf7fe8b49759b478228f9d78303522bc40f2948a1784d6c863373f10521dc80b61d35f201009e830d8a968cc989b37d2ad81
|
|
7
|
+
data.tar.gz: 3fdfda722d09ddea6c366ca4102258bdea6d36c923be729ae141063e7e62b43ae0730f30a6422ae4448f858a5c1d4269f80180279d7939ce050c88008613cedb
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
graphiti changelog
|
|
2
2
|
|
|
3
|
+
# [2.0.0-beta.10](https://github.com/graphiti-api/graphiti/compare/v2.0.0-beta.9...v2.0.0-beta.10) (2026-08-24)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* apply sideloads at definition once setup! has run ([39ef393](https://github.com/graphiti-api/graphiti/commit/39ef39383520816319282854c0ae99677bcd5127)), closes [#524](https://github.com/graphiti-api/graphiti/issues/524)
|
|
9
|
+
* give client-caused errors real detail and stop 500ing on bad query params ([35acc3e](https://github.com/graphiti-api/graphiti/commit/35acc3e6c78f68eeeb05728ea16264b136455c30)), closes [#531](https://github.com/graphiti-api/graphiti/issues/531)
|
|
10
|
+
* let an abstract resource declare relationships ([6036392](https://github.com/graphiti-api/graphiti/commit/6036392ebd611c7ed492beb3fe5754433eac9ffd)), closes [#453](https://github.com/graphiti-api/graphiti/issues/453)
|
|
11
|
+
* reject a non-object data payload instead of raising TypeError ([51603b8](https://github.com/graphiti-api/graphiti/commit/51603b854270715b80a75d9aa05a4020e6bbdce5))
|
|
12
|
+
* Update some missing sections and terms after renames, register unsupported pagination error ([39cff19](https://github.com/graphiti-api/graphiti/commit/39cff19f6f1f73126514c8b18234a18ad5eb7379))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* carry CurrentAttributes into concurrent sideloads ([7092096](https://github.com/graphiti-api/graphiti/commit/7092096f6d99ad277cc56817c616d946f655d55b))
|
|
18
|
+
* fold allow_nil and deny_empty into one blanks: filter option ([a108403](https://github.com/graphiti-api/graphiti/commit/a108403e5cd87fd2fdf177da30c4c4f97b9fe750))
|
|
19
|
+
* move link rendering onto the resource as page_links and relationship_links ([57878d1](https://github.com/graphiti-api/graphiti/commit/57878d14fdfc4741e8d418c8edbc4a8687335843))
|
|
20
|
+
* move typecast_reads onto the resource and let the railtie own Rails config ([236e970](https://github.com/graphiti-api/graphiti/commit/236e9706d7d8f08e9640c5c22ac596e2f7466849))
|
|
21
|
+
* one link mode per relationship, cascading from relationship_links ([f0e1da1](https://github.com/graphiti-api/graphiti/commit/f0e1da1e13a8d12b47458495b378779f5687ffe2))
|
|
22
|
+
* point connection pool timeouts at the sizing formula and add an audit task to check ([6e017c6](https://github.com/graphiti-api/graphiti/commit/6e017c60fd45338e4e6c8527adebfd46be4bfd8d))
|
|
23
|
+
* split validate_endpoints into validate_requests and validate_links ([100a370](https://github.com/graphiti-api/graphiti/commit/100a370deec6a0e6ae165bdf77e50b6b14d068d3))
|
|
24
|
+
|
|
3
25
|
# [2.0.0-beta.9](https://github.com/graphiti-api/graphiti/compare/v2.0.0-beta.8...v2.0.0-beta.9) (2026-08-22)
|
|
4
26
|
|
|
5
27
|
|
|
@@ -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
|
|
@@ -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 :as_literal, :as_nil, or :reject. Got #{@value.inspect}.
|
|
723
|
+
|
|
724
|
+
:as_literal - "null" and "" reach the filter as strings (default)
|
|
725
|
+
:as_nil - "null" becomes nil, so the filter can query for NULL
|
|
726
|
+
:reject - a blank value raises InvalidFilterValue
|
|
727
|
+
MSG
|
|
728
|
+
end
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
class InvalidLinkRendering < Base
|
|
732
|
+
def initialize(resource_class, attribute, value)
|
|
733
|
+
@resource_class = resource_class
|
|
734
|
+
@attribute = attribute
|
|
735
|
+
@value = value
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
def message
|
|
739
|
+
"#{@resource_class.name}: #{@attribute} must be true, false, or :on_demand. Got #{@value.inspect}."
|
|
740
|
+
end
|
|
741
|
+
end
|
|
742
|
+
|
|
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)
|
|
@@ -40,25 +43,39 @@ module Graphiti
|
|
|
40
43
|
!association?
|
|
41
44
|
end
|
|
42
45
|
|
|
43
|
-
def
|
|
44
|
-
return
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
def links_requested?
|
|
47
|
+
return @links_requested if defined?(@links_requested)
|
|
48
|
+
|
|
49
|
+
@links_requested = TRUTHY_LINKS.include?(@params[:links])
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def suppress_links?
|
|
53
|
+
return @suppress_links if defined?(@suppress_links)
|
|
54
|
+
|
|
55
|
+
@suppress_links = LINKLESS_FORMATS.include?(params[:format])
|
|
50
56
|
end
|
|
51
57
|
|
|
52
|
-
def
|
|
58
|
+
def render_link?(mode)
|
|
59
|
+
return false if suppress_links?
|
|
60
|
+
|
|
61
|
+
(mode == :on_demand) ? links_requested? : mode == true
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def page_links?
|
|
53
65
|
if action == :find
|
|
54
66
|
false
|
|
55
|
-
elsif
|
|
56
|
-
|
|
67
|
+
elsif @resource.page_links == :on_demand
|
|
68
|
+
page_links_requested?
|
|
57
69
|
else
|
|
58
|
-
|
|
70
|
+
@resource.page_links
|
|
59
71
|
end
|
|
60
72
|
end
|
|
61
73
|
|
|
74
|
+
def page_links_requested?
|
|
75
|
+
[@params[:page_links], @params[:pagination_links]]
|
|
76
|
+
.any? { |value| [true, "true"].include?(value) }
|
|
77
|
+
end
|
|
78
|
+
|
|
62
79
|
def debug_requested?
|
|
63
80
|
!!@params[:debug]
|
|
64
81
|
end
|
|
@@ -267,8 +284,8 @@ module Graphiti
|
|
|
267
284
|
def query_cache_key
|
|
268
285
|
attrs = {extra_fields: extra_fields,
|
|
269
286
|
fields: fields,
|
|
270
|
-
links:
|
|
271
|
-
|
|
287
|
+
links: suppress_links? ? :none : links_requested?,
|
|
288
|
+
page_links: page_links?,
|
|
272
289
|
format: params[:format]}
|
|
273
290
|
|
|
274
291
|
Digest::SHA1.hexdigest(attrs.to_s)
|
|
@@ -387,5 +404,9 @@ module Graphiti
|
|
|
387
404
|
action
|
|
388
405
|
end
|
|
389
406
|
end
|
|
407
|
+
|
|
408
|
+
alias_method :pagination_links?, :page_links?
|
|
409
|
+
DEPRECATOR.deprecate_methods(self,
|
|
410
|
+
pagination_links?: "Use `page_links?`")
|
|
390
411
|
end
|
|
391
412
|
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}"
|
|
@@ -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)
|
|
@@ -4,16 +4,75 @@ module Graphiti
|
|
|
4
4
|
extend ActiveSupport::Concern
|
|
5
5
|
|
|
6
6
|
DEFAULT_MAX_PAGE_SIZE = 1_000
|
|
7
|
+
LINK_MODES = [true, false, :on_demand].freeze
|
|
8
|
+
BELONGS_TO_RESOURCE_IDS_MODES = [:foreign_key, :always, :never].freeze
|
|
9
|
+
BLANK_MODES = [:as_literal, :as_nil, :reject].freeze
|
|
10
|
+
|
|
11
|
+
# Grouped for the ApplicationResource the install generator writes.
|
|
12
|
+
SETTING_GROUPS = { # :nodoc:
|
|
13
|
+
attributes: {
|
|
14
|
+
attributes_readable_by_default: {default: true},
|
|
15
|
+
attributes_writable_by_default: {default: true},
|
|
16
|
+
attributes_sortable_by_default: {default: true},
|
|
17
|
+
attributes_filterable_by_default: {default: true},
|
|
18
|
+
attributes_schema_by_default: {default: true},
|
|
19
|
+
typecast_reads: {default: true}
|
|
20
|
+
},
|
|
21
|
+
relationships: {
|
|
22
|
+
relationships_readable_by_default: {default: true},
|
|
23
|
+
relationships_writable_by_default: {default: true},
|
|
24
|
+
belongs_to_resource_ids_by_default: {
|
|
25
|
+
default: :foreign_key,
|
|
26
|
+
values: BELONGS_TO_RESOURCE_IDS_MODES,
|
|
27
|
+
invalid: ->(klass, value) { Errors::InvalidBelongsToResourceIds.new(klass, value) }
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
filters: {
|
|
31
|
+
filters_blanks_by_default: {
|
|
32
|
+
default: :as_literal,
|
|
33
|
+
values: BLANK_MODES,
|
|
34
|
+
invalid: ->(klass, value) { Errors::InvalidFilterBlanks.new(klass, :filters_blanks_by_default, value) }
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
sorting: {
|
|
38
|
+
default_sort: {default: nil, note: "per resource, e.g. [{id: :desc}]"}
|
|
39
|
+
},
|
|
40
|
+
pagination: {
|
|
41
|
+
page_default_size: {default: nil, note: "unset falls back to 20"},
|
|
42
|
+
page_max_size: {default: DEFAULT_MAX_PAGE_SIZE, format: "1_000"},
|
|
43
|
+
page_cursors: {default: false, note: "render cursors for pagination"},
|
|
44
|
+
page_links: {
|
|
45
|
+
default: false,
|
|
46
|
+
values: LINK_MODES,
|
|
47
|
+
invalid: ->(klass, value) { Errors::InvalidLinkRendering.new(klass, :page_links, value) }
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
endpoints: {
|
|
51
|
+
validate_requests: {default: true, note: "refuse requests to undeclared endpoints"}
|
|
52
|
+
},
|
|
53
|
+
links: {
|
|
54
|
+
relationship_links: {
|
|
55
|
+
default: true,
|
|
56
|
+
values: LINK_MODES,
|
|
57
|
+
invalid: ->(klass, value) { Errors::InvalidLinkRendering.new(klass, :relationship_links, value) }
|
|
58
|
+
},
|
|
59
|
+
validate_links: {default: true, note: "refuse to render links to unroutable endpoints"}
|
|
60
|
+
}
|
|
61
|
+
}.freeze
|
|
62
|
+
|
|
63
|
+
SETTINGS = SETTING_GROUPS.values.reduce(:merge).freeze # :nodoc:
|
|
7
64
|
|
|
8
65
|
module Overrides
|
|
9
|
-
|
|
66
|
+
SETTINGS.each_pair do |name, setting|
|
|
67
|
+
next unless setting[:values]
|
|
10
68
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
69
|
+
define_method(:"#{name}=") do |val|
|
|
70
|
+
unless setting[:values].include?(val)
|
|
71
|
+
raise setting[:invalid].call(self, val)
|
|
72
|
+
end
|
|
15
73
|
|
|
16
|
-
|
|
74
|
+
super(val)
|
|
75
|
+
end
|
|
17
76
|
end
|
|
18
77
|
|
|
19
78
|
def serializer=(val)
|
|
@@ -95,51 +154,37 @@ module Graphiti
|
|
|
95
154
|
:polymorphic,
|
|
96
155
|
:polymorphic_child,
|
|
97
156
|
:serializer,
|
|
98
|
-
:default_page_size,
|
|
99
|
-
:default_sort,
|
|
100
|
-
:max_page_size,
|
|
101
|
-
:attributes_readable_by_default,
|
|
102
|
-
:attributes_writable_by_default,
|
|
103
|
-
:attributes_sortable_by_default,
|
|
104
|
-
:attributes_filterable_by_default,
|
|
105
|
-
:attributes_schema_by_default,
|
|
106
|
-
:relationships_readable_by_default,
|
|
107
|
-
:relationships_writable_by_default,
|
|
108
|
-
:belongs_to_resource_ids_by_default,
|
|
109
|
-
:filters_accept_nil_by_default,
|
|
110
|
-
:filters_deny_empty_by_default,
|
|
111
157
|
:graphql_entrypoint,
|
|
112
|
-
|
|
158
|
+
*SETTINGS.keys
|
|
113
159
|
|
|
114
160
|
class << self
|
|
115
161
|
prepend Overrides
|
|
116
162
|
end
|
|
117
163
|
|
|
164
|
+
SETTINGS.each_pair do |name, setting|
|
|
165
|
+
public_send(:"#{name}=", setting[:default])
|
|
166
|
+
end
|
|
167
|
+
|
|
118
168
|
def self.inherited(klass)
|
|
119
169
|
super
|
|
120
170
|
klass.config = Util::Hash.deep_dup(config)
|
|
121
171
|
klass.adapter ||= Adapters::Abstract
|
|
122
|
-
klass.max_page_size ||= DEFAULT_MAX_PAGE_SIZE
|
|
123
172
|
# re-assigning causes a new Class.new
|
|
124
173
|
klass.serializer = (klass.serializer || klass.infer_serializer_superclass)
|
|
125
174
|
klass.type ||= klass.infer_type
|
|
126
175
|
klass.graphql_entrypoint = klass.type.to_s.pluralize.to_sym
|
|
127
|
-
default(klass, :attributes_readable_by_default, true)
|
|
128
|
-
default(klass, :attributes_writable_by_default, true)
|
|
129
|
-
default(klass, :attributes_sortable_by_default, true)
|
|
130
|
-
default(klass, :attributes_filterable_by_default, true)
|
|
131
|
-
default(klass, :attributes_schema_by_default, true)
|
|
132
|
-
default(klass, :relationships_readable_by_default, true)
|
|
133
|
-
default(klass, :relationships_writable_by_default, true)
|
|
134
|
-
default(klass, :belongs_to_resource_ids_by_default, :foreign_key)
|
|
135
|
-
default(klass, :filters_accept_nil_by_default, false)
|
|
136
|
-
default(klass, :filters_deny_empty_by_default, false)
|
|
137
|
-
|
|
138
176
|
unless klass.config[:attributes][:id]
|
|
139
177
|
klass.attribute :id, :integer_id
|
|
140
178
|
end
|
|
141
179
|
klass.stat total: [:count]
|
|
142
180
|
|
|
181
|
+
# An abstract parent has no serializer for its sideloads, so the subclass applies them here.
|
|
182
|
+
if abstract_class?
|
|
183
|
+
klass.config[:sideloads].each_pair do |name, sideload|
|
|
184
|
+
klass.apply_sideload_to_serializer(name) if klass.eagerly_apply_sideload?(sideload)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
143
188
|
if defined?(::Rails) && ::Rails.env.development?
|
|
144
189
|
# Avoid adding dupe resources when re-autoloading
|
|
145
190
|
Graphiti.resources.reject! { |r| r.name == klass.name }
|
|
@@ -149,6 +194,52 @@ module Graphiti
|
|
|
149
194
|
end
|
|
150
195
|
|
|
151
196
|
class_methods do
|
|
197
|
+
# Deprecated. Both folded into filters_blanks_by_default. Remove in 3.0.
|
|
198
|
+
def filters_accept_nil_by_default
|
|
199
|
+
filters_blanks_by_default == :as_nil
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def filters_accept_nil_by_default=(val)
|
|
203
|
+
self.filters_blanks_by_default = val ? :as_nil : :as_literal
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def filters_deny_empty_by_default
|
|
207
|
+
filters_blanks_by_default == :reject
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def filters_deny_empty_by_default=(val)
|
|
211
|
+
self.filters_blanks_by_default = val ? :reject : :as_literal
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Deprecated. Renamed to the page_ family. Remove in 3.0.
|
|
215
|
+
def default_page_size
|
|
216
|
+
page_default_size
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def default_page_size=(val)
|
|
220
|
+
self.page_default_size = val
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def max_page_size
|
|
224
|
+
page_max_size
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def max_page_size=(val)
|
|
228
|
+
self.page_max_size = val
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def cursor_paginatable
|
|
232
|
+
page_cursors
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def cursor_paginatable=(val)
|
|
236
|
+
self.page_cursors = val
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def cursor_paginatable?
|
|
240
|
+
!!page_cursors
|
|
241
|
+
end
|
|
242
|
+
|
|
152
243
|
def get_attr!(name, flag, opts = {})
|
|
153
244
|
opts[:raise_error] = true
|
|
154
245
|
get_attr(name, flag, opts)
|
|
@@ -205,14 +296,6 @@ module Graphiti
|
|
|
205
296
|
serializer_class
|
|
206
297
|
end
|
|
207
298
|
|
|
208
|
-
def default(object, attr, value)
|
|
209
|
-
prior = object.send(attr)
|
|
210
|
-
unless prior || prior == false
|
|
211
|
-
object.send(:"#{attr}=", value)
|
|
212
|
-
end
|
|
213
|
-
end
|
|
214
|
-
private :default
|
|
215
|
-
|
|
216
299
|
def config
|
|
217
300
|
@config ||=
|
|
218
301
|
{
|
|
@@ -332,5 +415,20 @@ module Graphiti
|
|
|
332
415
|
self.class.default_filters
|
|
333
416
|
end
|
|
334
417
|
end
|
|
418
|
+
|
|
419
|
+
blanks_msg = "Use `self.filters_blanks_by_default` (:as_literal, :as_nil, or :reject)"
|
|
420
|
+
page_msg = "Use `self.page_default_size`, `self.page_max_size` and `self.page_cursors`"
|
|
421
|
+
DEPRECATOR.deprecate_methods(Configuration::ClassMethods,
|
|
422
|
+
filters_accept_nil_by_default: blanks_msg,
|
|
423
|
+
"filters_accept_nil_by_default=": blanks_msg,
|
|
424
|
+
filters_deny_empty_by_default: blanks_msg,
|
|
425
|
+
"filters_deny_empty_by_default=": blanks_msg,
|
|
426
|
+
default_page_size: page_msg,
|
|
427
|
+
"default_page_size=": page_msg,
|
|
428
|
+
max_page_size: page_msg,
|
|
429
|
+
"max_page_size=": page_msg,
|
|
430
|
+
cursor_paginatable: page_msg,
|
|
431
|
+
"cursor_paginatable=": page_msg,
|
|
432
|
+
cursor_paginatable?: page_msg)
|
|
335
433
|
end
|
|
336
434
|
end
|
|
@@ -40,8 +40,7 @@ module Graphiti
|
|
|
40
40
|
required: required,
|
|
41
41
|
schema: schema,
|
|
42
42
|
operators: operators.to_hash,
|
|
43
|
-
|
|
44
|
-
deny_empty: opts.fetch(:deny_empty, filters_deny_empty_by_default)
|
|
43
|
+
blanks: blanks_for(name, opts)
|
|
45
44
|
}
|
|
46
45
|
elsif (type = args[0])
|
|
47
46
|
attribute name, type, only: [:filterable], allow: opts[:allow]
|
|
@@ -206,6 +205,26 @@ module Graphiti
|
|
|
206
205
|
end
|
|
207
206
|
end
|
|
208
207
|
private :relationship_option
|
|
208
|
+
|
|
209
|
+
def blanks_for(name, opts)
|
|
210
|
+
if opts.key?(:deny_empty)
|
|
211
|
+
DEPRECATOR.warn("The deny_empty: filter option is deprecated. Use blanks: :reject.")
|
|
212
|
+
return :reject if opts[:deny_empty]
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
if opts.key?(:allow_nil)
|
|
216
|
+
DEPRECATOR.warn("The allow_nil: filter option is deprecated. Use blanks: :as_nil.")
|
|
217
|
+
return opts[:allow_nil] ? :as_nil : :as_literal
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
blanks = opts.fetch(:blanks, filters_blanks_by_default)
|
|
221
|
+
unless Resource::BLANK_MODES.include?(blanks)
|
|
222
|
+
raise Errors::InvalidFilterBlanks.new(self, name, blanks)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
blanks
|
|
226
|
+
end
|
|
227
|
+
private :blanks_for
|
|
209
228
|
end
|
|
210
229
|
end
|
|
211
230
|
end
|
|
@@ -80,7 +80,7 @@ module Graphiti
|
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
def validate_request!(params)
|
|
83
|
-
return if Graphiti.context[:graphql] || !
|
|
83
|
+
return if Graphiti.context[:graphql] || !validate_requests?
|
|
84
84
|
|
|
85
85
|
if context&.respond_to?(:request)
|
|
86
86
|
path = context.request.env["PATH_INFO"]
|
|
@@ -25,12 +25,8 @@ module Graphiti
|
|
|
25
25
|
class_attribute :endpoint,
|
|
26
26
|
:base_url,
|
|
27
27
|
:endpoint_namespace,
|
|
28
|
-
:secondary_endpoints
|
|
29
|
-
:autolink,
|
|
30
|
-
:validate_endpoints
|
|
28
|
+
:secondary_endpoints
|
|
31
29
|
self.secondary_endpoints = []
|
|
32
|
-
self.autolink = true
|
|
33
|
-
self.validate_endpoints = true
|
|
34
30
|
|
|
35
31
|
class << self
|
|
36
32
|
prepend Overrides
|
|
@@ -38,6 +34,33 @@ module Graphiti
|
|
|
38
34
|
end
|
|
39
35
|
|
|
40
36
|
class_methods do
|
|
37
|
+
# Deprecated. Folded into relationship_links. Remove in 3.0.
|
|
38
|
+
def autolink
|
|
39
|
+
relationship_links != false
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def autolink=(val)
|
|
43
|
+
self.relationship_links = val ? true : false
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def autolink?
|
|
47
|
+
autolink
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Deprecated. Split into validate_requests and validate_links. Remove in 3.0.
|
|
51
|
+
def validate_endpoints
|
|
52
|
+
validate_requests && validate_links
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def validate_endpoints=(val)
|
|
56
|
+
self.validate_requests = val
|
|
57
|
+
self.validate_links = val
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def validate_endpoints?
|
|
61
|
+
validate_endpoints
|
|
62
|
+
end
|
|
63
|
+
|
|
41
64
|
def infer_endpoint
|
|
42
65
|
return unless name
|
|
43
66
|
|
|
@@ -101,4 +124,12 @@ module Graphiti
|
|
|
101
124
|
end
|
|
102
125
|
end
|
|
103
126
|
end
|
|
127
|
+
|
|
128
|
+
msg = "Use `self.relationship_links` (true, false, or :on_demand)"
|
|
129
|
+
endpoints_msg = "Use `self.validate_requests` for inbound requests, `self.validate_links` for rendered links"
|
|
130
|
+
DEPRECATOR.deprecate_methods(Links::ClassMethods,
|
|
131
|
+
autolink: msg, "autolink=": msg, autolink?: msg,
|
|
132
|
+
validate_endpoints: endpoints_msg,
|
|
133
|
+
"validate_endpoints=": endpoints_msg,
|
|
134
|
+
validate_endpoints?: endpoints_msg)
|
|
104
135
|
end
|
|
@@ -126,11 +126,11 @@ module Graphiti
|
|
|
126
126
|
end
|
|
127
127
|
|
|
128
128
|
# If eager loading, ensure routes are loaded first, then apply
|
|
129
|
-
# This happens in Railtie
|
|
129
|
+
# This happens in Railtie. Setup runs once, so classes redefined by a reload apply at definition.
|
|
130
130
|
def eagerly_apply_sideload?(sideload)
|
|
131
131
|
# TODO: Maybe handle this in the Rails integration
|
|
132
132
|
if defined?(::Rails) && (app = ::Rails.application)
|
|
133
|
-
app.config.eager_load ?
|
|
133
|
+
app.config.eager_load ? Graphiti.setup? : true
|
|
134
134
|
else
|
|
135
135
|
sideload.resource_class_loaded?
|
|
136
136
|
end
|
data/lib/graphiti/schema.rb
CHANGED
|
@@ -111,8 +111,8 @@ module Graphiti
|
|
|
111
111
|
config[:default_sort] = default_sort
|
|
112
112
|
end
|
|
113
113
|
|
|
114
|
-
if r.
|
|
115
|
-
config[:default_page_size] = r.
|
|
114
|
+
if r.page_default_size
|
|
115
|
+
config[:default_page_size] = r.page_default_size
|
|
116
116
|
end
|
|
117
117
|
|
|
118
118
|
if r.polymorphic? && !r.polymorphic_child?
|
data/lib/graphiti/scope.rb
CHANGED
|
@@ -257,15 +257,21 @@ module Graphiti
|
|
|
257
257
|
end
|
|
258
258
|
end
|
|
259
259
|
|
|
260
|
+
current_attributes = current_attributes_snapshot
|
|
261
|
+
|
|
260
262
|
Concurrent::Promises.future_on(
|
|
261
|
-
self.class.global_thread_pool_executor, Thread.current.object_id, thread_storage, fiber_storage, *args
|
|
262
|
-
) do |thread_id, thread_storage, fiber_storage, *args|
|
|
263
|
+
self.class.global_thread_pool_executor, Thread.current.object_id, thread_storage, fiber_storage, current_attributes, *args
|
|
264
|
+
) do |thread_id, thread_storage, fiber_storage, current_attributes, *args|
|
|
263
265
|
self.class.marking_pool_thread do
|
|
264
266
|
wrap_in_rails_executor do
|
|
265
267
|
with_thread_locals(thread_storage) do
|
|
266
268
|
with_fiber_locals(fiber_storage) do
|
|
267
|
-
|
|
268
|
-
|
|
269
|
+
with_current_attributes(current_attributes) do
|
|
270
|
+
with_connection_pool_hint do
|
|
271
|
+
Graphiti.broadcast(:global_thread_pool_task_run, self.class.global_thread_pool_stats) do
|
|
272
|
+
yield(*args)
|
|
273
|
+
end
|
|
274
|
+
end
|
|
269
275
|
end
|
|
270
276
|
end
|
|
271
277
|
end
|
|
@@ -274,6 +280,45 @@ module Graphiti
|
|
|
274
280
|
end
|
|
275
281
|
end
|
|
276
282
|
|
|
283
|
+
def current_attributes_snapshot
|
|
284
|
+
return unless defined?(ActiveSupport::CurrentAttributes)
|
|
285
|
+
|
|
286
|
+
snapshot = {}
|
|
287
|
+
klasses = ActiveSupport::CurrentAttributes.subclasses
|
|
288
|
+
while (klass = klasses.shift)
|
|
289
|
+
klasses.concat(klass.subclasses)
|
|
290
|
+
attributes = klass.attributes
|
|
291
|
+
snapshot[klass] = attributes.dup if attributes.any?
|
|
292
|
+
end
|
|
293
|
+
snapshot
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Restored inside the Rails executor, whose entry hands the pool thread a
|
|
297
|
+
# fresh Current. Restoring through set scopes the values to the block.
|
|
298
|
+
def with_current_attributes(snapshot, &block)
|
|
299
|
+
return yield if snapshot.nil? || snapshot.empty?
|
|
300
|
+
|
|
301
|
+
klass, attributes = snapshot.first
|
|
302
|
+
rest = snapshot.except(klass)
|
|
303
|
+
klass.set(attributes) { with_current_attributes(rest, &block) }
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# A timeout inside a sideload task nearly always means database.yml's pool
|
|
307
|
+
# was not sized for the sideload threads, and the bare error does not say so.
|
|
308
|
+
def with_connection_pool_hint
|
|
309
|
+
yield
|
|
310
|
+
rescue => error
|
|
311
|
+
raise unless defined?(ActiveRecord::ConnectionTimeoutError) && error.is_a?(ActiveRecord::ConnectionTimeoutError)
|
|
312
|
+
|
|
313
|
+
hinted = error.exception(<<~MSG.strip)
|
|
314
|
+
#{error.message}
|
|
315
|
+
|
|
316
|
+
Raised while resolving a sideload concurrently. Each concurrent sideload holds its own database connection, so `pool` in database.yml must be at least web threads + concurrency_max_threads (#{Graphiti.config.concurrency_max_threads}) + 1. See graphiti.dev/concepts/resources#concurrency-pool-sizing.
|
|
317
|
+
MSG
|
|
318
|
+
hinted.set_backtrace(error.backtrace)
|
|
319
|
+
raise hinted
|
|
320
|
+
end
|
|
321
|
+
|
|
277
322
|
def with_thread_locals(thread_locals)
|
|
278
323
|
new_thread_locals = []
|
|
279
324
|
thread_locals.each do |key, value|
|
|
@@ -62,7 +62,7 @@ module Graphiti
|
|
|
62
62
|
value = parse_string_value(filter.values[0], value)
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
check_blank_filters!(resource, filter, value)
|
|
66
66
|
value = parse_string_null(filter.values[0], value)
|
|
67
67
|
validate_singular(resource, filter, value)
|
|
68
68
|
value = coerce_types(filter.values[0], param_name.to_sym, value)
|
|
@@ -209,15 +209,15 @@ module Graphiti
|
|
|
209
209
|
end
|
|
210
210
|
|
|
211
211
|
def parse_string_null(filter, value)
|
|
212
|
-
return value unless filter[:
|
|
212
|
+
return value unless filter[:blanks] == :as_nil
|
|
213
213
|
return value.map { |item| (item == "null") ? nil : item } if value.is_a?(Array)
|
|
214
214
|
return if value == "null"
|
|
215
215
|
|
|
216
216
|
value
|
|
217
217
|
end
|
|
218
218
|
|
|
219
|
-
def
|
|
220
|
-
return unless filter.values[0][:
|
|
219
|
+
def check_blank_filters!(resource, filter, value)
|
|
220
|
+
return unless filter.values[0][:blanks] == :reject
|
|
221
221
|
|
|
222
222
|
if value.nil? || value.empty? || value == "null"
|
|
223
223
|
raise Errors::InvalidFilterValue.new(resource, filter, "(empty)")
|
|
@@ -4,9 +4,9 @@ module Graphiti
|
|
|
4
4
|
PARAMS = [:number, :size, :offset, :before, :after]
|
|
5
5
|
|
|
6
6
|
def apply
|
|
7
|
-
if size > resource.
|
|
7
|
+
if size > resource.page_max_size
|
|
8
8
|
raise Graphiti::Errors::UnsupportedPageSize
|
|
9
|
-
.new(size, resource.
|
|
9
|
+
.new(size, resource.page_max_size)
|
|
10
10
|
elsif requested? && @opts[:sideload_parent_length].to_i > 1
|
|
11
11
|
raise Graphiti::Errors::UnsupportedPagination
|
|
12
12
|
else
|
|
@@ -100,7 +100,7 @@ module Graphiti
|
|
|
100
100
|
end
|
|
101
101
|
|
|
102
102
|
def size
|
|
103
|
-
(page_param[:size] || resource.
|
|
103
|
+
(page_param[:size] || resource.page_default_size || DEFAULT_PAGE_SIZE).to_i
|
|
104
104
|
end
|
|
105
105
|
end
|
|
106
106
|
end
|
data/lib/graphiti/serializer.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
module Graphiti
|
|
2
2
|
class Serializer < JSONAPI::Serializable::Resource
|
|
3
|
+
UNREQUESTED_LINKS = [false, nil, "false"].freeze
|
|
4
|
+
|
|
3
5
|
include Graphiti::Extensions::BooleanAttribute
|
|
4
6
|
include Graphiti::Extensions::ExtraAttribute
|
|
5
7
|
include Graphiti::SerializableHash
|
|
@@ -59,7 +61,7 @@ module Graphiti
|
|
|
59
61
|
starting_offset = 0
|
|
60
62
|
page_param = @proxy.query.pagination
|
|
61
63
|
if (page_number = page_param[:number])
|
|
62
|
-
page_size = page_param[:size] || @resource.
|
|
64
|
+
page_size = page_param[:size] || @resource.page_default_size
|
|
63
65
|
starting_offset = (page_number - 1) * page_size
|
|
64
66
|
end
|
|
65
67
|
|
|
@@ -100,17 +102,21 @@ module Graphiti
|
|
|
100
102
|
hash[:links] = @resource.links(@object) if @resource.links?
|
|
101
103
|
end
|
|
102
104
|
|
|
105
|
+
# A relationship whose only content would be an unrequested on-demand link
|
|
106
|
+
# serializes as an empty stub, which JSON:API forbids.
|
|
103
107
|
def strip_relationships!(hash)
|
|
104
|
-
hash[:relationships]&.
|
|
105
|
-
payload.key?(:data)
|
|
108
|
+
hash[:relationships]&.reject! do |name, payload|
|
|
109
|
+
next false if payload.key?(:data) || payload.key?(:links)
|
|
110
|
+
|
|
111
|
+
self.class.relationship_sideloads[name]&.link_mode == :on_demand
|
|
106
112
|
end
|
|
107
113
|
end
|
|
108
114
|
|
|
109
115
|
def strip_relationships?
|
|
110
|
-
|
|
111
|
-
params =
|
|
116
|
+
context = Graphiti.context[:object]
|
|
117
|
+
params = context.params if context.respond_to?(:params)
|
|
112
118
|
|
|
113
|
-
|
|
119
|
+
UNREQUESTED_LINKS.include?(params && params[:links])
|
|
114
120
|
end
|
|
115
121
|
end
|
|
116
122
|
end
|
data/lib/graphiti/sideload.rb
CHANGED
|
@@ -31,6 +31,9 @@ module Graphiti
|
|
|
31
31
|
@writable = opts[:writable]
|
|
32
32
|
@as = opts[:as]
|
|
33
33
|
@link = opts[:link]
|
|
34
|
+
unless @link.nil? || Resource::LINK_MODES.include?(@link)
|
|
35
|
+
raise Errors::InvalidLinkRendering.new(@parent_resource_class, :"#{name} link", @link)
|
|
36
|
+
end
|
|
34
37
|
@single = opts[:single]
|
|
35
38
|
@remote = opts[:remote]
|
|
36
39
|
apply_belongs_to_many_filter if type == :many_to_many
|
|
@@ -84,7 +87,8 @@ module Graphiti
|
|
|
84
87
|
self.adapter = Graphiti::Adapters::GraphitiAPI
|
|
85
88
|
self.model = OpenStruct
|
|
86
89
|
self.remote = remote_url
|
|
87
|
-
self.
|
|
90
|
+
self.validate_requests = false
|
|
91
|
+
self.validate_links = false
|
|
88
92
|
}
|
|
89
93
|
name = "#{parent_resource_class.name}.#{@name}.remote"
|
|
90
94
|
klass.class_eval("def self.name;'#{name}';end", __FILE__, __LINE__)
|
|
@@ -166,14 +170,16 @@ module Graphiti
|
|
|
166
170
|
false
|
|
167
171
|
end
|
|
168
172
|
|
|
169
|
-
|
|
170
|
-
|
|
173
|
+
# A custom link block means the author wants the link, so a false default does not silence it.
|
|
174
|
+
def link_mode
|
|
175
|
+
return @link unless @link.nil?
|
|
171
176
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
+
default = @parent_resource_class.relationship_links
|
|
178
|
+
(link_proc && default == false) ? true : default
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def link?
|
|
182
|
+
link_mode != false
|
|
177
183
|
end
|
|
178
184
|
|
|
179
185
|
def link_filter(parents)
|
|
@@ -140,7 +140,7 @@ module Graphiti
|
|
|
140
140
|
end
|
|
141
141
|
|
|
142
142
|
class FilterAttributeMatcher < ResourceDSLMatcher
|
|
143
|
-
GRAPHITI_OPTS = %i[allow deny single required
|
|
143
|
+
GRAPHITI_OPTS = %i[allow deny single required blanks].freeze
|
|
144
144
|
GRAPHITI_CONFIG_KEY = :filters
|
|
145
145
|
EXPECTED_ACTION = "filter"
|
|
146
146
|
end
|
|
@@ -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(
|
|
189
|
+
# @example expect(subject).to filter_attribute(:name, :string).with_options(blanks: :as_nil)
|
|
190
190
|
# @example expect(subject).not_to filter_attribute(:name, :string)
|
|
191
191
|
def filter_attribute(attribute, type)
|
|
192
192
|
FilterAttributeMatcher.new(attribute, type)
|
|
@@ -39,14 +39,17 @@ resource_testing = proc do
|
|
|
39
39
|
let(:params) { {} }
|
|
40
40
|
|
|
41
41
|
around do |e|
|
|
42
|
-
|
|
43
|
-
Graphiti::Resource.
|
|
42
|
+
original_requests = Graphiti::Resource.validate_requests
|
|
43
|
+
original_links = Graphiti::Resource.validate_links
|
|
44
|
+
Graphiti::Resource.validate_requests = false
|
|
45
|
+
Graphiti::Resource.validate_links = false
|
|
44
46
|
|
|
45
47
|
Graphiti.with_context graphiti_context do
|
|
46
48
|
e.run
|
|
47
49
|
end
|
|
48
50
|
ensure
|
|
49
|
-
Graphiti::Resource.
|
|
51
|
+
Graphiti::Resource.validate_requests = original_requests
|
|
52
|
+
Graphiti::Resource.validate_links = original_links
|
|
50
53
|
end
|
|
51
54
|
|
|
52
55
|
def graphiti_context
|
data/lib/graphiti/util/link.rb
CHANGED
|
@@ -30,7 +30,7 @@ module Graphiti
|
|
|
30
30
|
@serializer.send(:"#{applied_method}=", [@name] | existing)
|
|
31
31
|
|
|
32
32
|
@serializer.meta do
|
|
33
|
-
if !!@resource.try(:
|
|
33
|
+
if !!@resource.try(:page_cursors?) && !Graphiti.context[:graphql]
|
|
34
34
|
{cursor: cursor}
|
|
35
35
|
end
|
|
36
36
|
end
|
|
@@ -121,10 +121,11 @@ module Graphiti
|
|
|
121
121
|
|
|
122
122
|
def default_proc
|
|
123
123
|
name_ref = @name
|
|
124
|
+
resource_ref = @resource
|
|
124
125
|
typecast_ref = typecast(Graphiti::Types[@attr[:type]][:read])
|
|
125
126
|
->(_) {
|
|
126
127
|
val = @object.send(name_ref)
|
|
127
|
-
if
|
|
128
|
+
if resource_ref.typecast_reads
|
|
128
129
|
typecast_ref.call(val)
|
|
129
130
|
else
|
|
130
131
|
val
|
|
@@ -133,10 +134,11 @@ module Graphiti
|
|
|
133
134
|
end
|
|
134
135
|
|
|
135
136
|
def wrap_proc(inner)
|
|
137
|
+
resource_ref = @resource
|
|
136
138
|
typecast_ref = typecast(Graphiti::Types[@attr[:type]][:read])
|
|
137
139
|
->(serializer_instance = nil) {
|
|
138
140
|
val = serializer_instance.instance_eval(&inner)
|
|
139
|
-
if
|
|
141
|
+
if resource_ref.typecast_reads
|
|
140
142
|
typecast_ref.call(val)
|
|
141
143
|
else
|
|
142
144
|
val
|
|
@@ -8,6 +8,8 @@ module Graphiti
|
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def apply
|
|
11
|
+
return unless @serializer
|
|
12
|
+
|
|
11
13
|
@sideloads.each_pair do |name, sideload|
|
|
12
14
|
if apply?(sideload)
|
|
13
15
|
SerializerRelationship
|
|
@@ -55,7 +57,6 @@ module Graphiti
|
|
|
55
57
|
private
|
|
56
58
|
|
|
57
59
|
def block
|
|
58
|
-
link_ref = link?
|
|
59
60
|
sideload_ref = @sideload
|
|
60
61
|
resource_class_ref = @resource_class
|
|
61
62
|
data_proc_ref = data_proc
|
|
@@ -93,13 +94,11 @@ module Graphiti
|
|
|
93
94
|
linkage always: sideload_ref.render_resource_ids?
|
|
94
95
|
end
|
|
95
96
|
|
|
96
|
-
if
|
|
97
|
-
|
|
98
|
-
self_ref.send(:validate_link!) unless self_ref.send(:eagerly_validate_links?)
|
|
97
|
+
if @proxy.query.render_link?(sideload_ref.link_mode) && self_ref.send(:linkable?)
|
|
98
|
+
self_ref.send(:validate_link!) unless self_ref.send(:eagerly_validate_links?)
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
end
|
|
100
|
+
link(:related) do
|
|
101
|
+
::Graphiti::Util::Link.new(sideload_ref, @object).generate
|
|
103
102
|
end
|
|
104
103
|
end
|
|
105
104
|
end
|
|
@@ -162,8 +161,8 @@ module Graphiti
|
|
|
162
161
|
end
|
|
163
162
|
|
|
164
163
|
def validate_link!
|
|
165
|
-
return unless link?
|
|
166
|
-
return unless @resource_class.
|
|
164
|
+
return unless @sideload.link? && linkable?
|
|
165
|
+
return unless @resource_class.validate_links?
|
|
167
166
|
return if @sideload.link_proc
|
|
168
167
|
|
|
169
168
|
unless Graphiti.config.context_for_endpoint
|
|
@@ -192,14 +191,16 @@ module Graphiti
|
|
|
192
191
|
self.class.validated_link_cache << cache_key
|
|
193
192
|
end
|
|
194
193
|
|
|
195
|
-
|
|
196
|
-
|
|
194
|
+
# Checked lazily so a sideload with no endpoint only raises when a link is actually wanted.
|
|
195
|
+
def linkable?
|
|
196
|
+
return @linkable if defined?(@linkable)
|
|
197
197
|
|
|
198
|
-
if @sideload.
|
|
199
|
-
|
|
200
|
-
|
|
198
|
+
@linkable = if @sideload.link_proc
|
|
199
|
+
true
|
|
200
|
+
elsif @sideload.respond_to?(:children)
|
|
201
|
+
@sideload.children.values.all? { |c| !c.resource.endpoint.nil? }
|
|
201
202
|
else
|
|
202
|
-
|
|
203
|
+
!@sideload.resource.endpoint.nil?
|
|
203
204
|
end
|
|
204
205
|
end
|
|
205
206
|
end
|
data/lib/graphiti/version.rb
CHANGED
data/lib/graphiti.rb
CHANGED
data/lib/tasks/graphiti.rake
CHANGED