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
|
@@ -4,16 +4,76 @@ 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 = [:literal, :null, :rejected].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
|
+
relationship_placeholders: {default: false, note: "render placeholders for relationships with no ids and no link"},
|
|
25
|
+
belongs_to_resource_ids_by_default: {
|
|
26
|
+
default: :foreign_key,
|
|
27
|
+
values: BELONGS_TO_RESOURCE_IDS_MODES,
|
|
28
|
+
invalid: ->(klass, value) { Errors::InvalidBelongsToResourceIds.new(klass, value) }
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
filters: {
|
|
32
|
+
filter_blanks_treated_as: {
|
|
33
|
+
default: :literal,
|
|
34
|
+
values: BLANK_MODES,
|
|
35
|
+
invalid: ->(klass, value) { Errors::InvalidFilterBlanks.new(klass, :filter_blanks_treated_as, value) }
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
sorting: {
|
|
39
|
+
default_sort: {default: nil, note: "per resource, e.g. [{id: :desc}]"}
|
|
40
|
+
},
|
|
41
|
+
pagination: {
|
|
42
|
+
page_default_size: {default: nil, note: "unset falls back to 20"},
|
|
43
|
+
page_max_size: {default: DEFAULT_MAX_PAGE_SIZE, format: "1_000"},
|
|
44
|
+
page_cursors: {default: false, note: "render cursors for pagination"},
|
|
45
|
+
page_links: {
|
|
46
|
+
default: false,
|
|
47
|
+
values: LINK_MODES,
|
|
48
|
+
invalid: ->(klass, value) { Errors::InvalidLinkRendering.new(klass, :page_links, value) }
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
endpoints: {
|
|
52
|
+
validate_requests: {default: true, note: "refuse requests to undeclared endpoints"}
|
|
53
|
+
},
|
|
54
|
+
links: {
|
|
55
|
+
relationship_links: {
|
|
56
|
+
default: true,
|
|
57
|
+
values: LINK_MODES,
|
|
58
|
+
invalid: ->(klass, value) { Errors::InvalidLinkRendering.new(klass, :relationship_links, value) }
|
|
59
|
+
},
|
|
60
|
+
validate_links: {default: true, note: "refuse to render links to unroutable endpoints"}
|
|
61
|
+
}
|
|
62
|
+
}.freeze
|
|
63
|
+
|
|
64
|
+
SETTINGS = SETTING_GROUPS.values.reduce(:merge).freeze # :nodoc:
|
|
7
65
|
|
|
8
66
|
module Overrides
|
|
9
|
-
|
|
67
|
+
SETTINGS.each_pair do |name, setting|
|
|
68
|
+
next unless setting[:values]
|
|
10
69
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
70
|
+
define_method(:"#{name}=") do |val|
|
|
71
|
+
unless setting[:values].include?(val)
|
|
72
|
+
raise setting[:invalid].call(self, val)
|
|
73
|
+
end
|
|
15
74
|
|
|
16
|
-
|
|
75
|
+
super(val)
|
|
76
|
+
end
|
|
17
77
|
end
|
|
18
78
|
|
|
19
79
|
def serializer=(val)
|
|
@@ -95,51 +155,37 @@ module Graphiti
|
|
|
95
155
|
:polymorphic,
|
|
96
156
|
:polymorphic_child,
|
|
97
157
|
: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
158
|
:graphql_entrypoint,
|
|
112
|
-
|
|
159
|
+
*SETTINGS.keys
|
|
113
160
|
|
|
114
161
|
class << self
|
|
115
162
|
prepend Overrides
|
|
116
163
|
end
|
|
117
164
|
|
|
165
|
+
SETTINGS.each_pair do |name, setting|
|
|
166
|
+
public_send(:"#{name}=", setting[:default])
|
|
167
|
+
end
|
|
168
|
+
|
|
118
169
|
def self.inherited(klass)
|
|
119
170
|
super
|
|
120
171
|
klass.config = Util::Hash.deep_dup(config)
|
|
121
172
|
klass.adapter ||= Adapters::Abstract
|
|
122
|
-
klass.max_page_size ||= DEFAULT_MAX_PAGE_SIZE
|
|
123
173
|
# re-assigning causes a new Class.new
|
|
124
174
|
klass.serializer = (klass.serializer || klass.infer_serializer_superclass)
|
|
125
175
|
klass.type ||= klass.infer_type
|
|
126
176
|
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
177
|
unless klass.config[:attributes][:id]
|
|
139
178
|
klass.attribute :id, :integer_id
|
|
140
179
|
end
|
|
141
180
|
klass.stat total: [:count]
|
|
142
181
|
|
|
182
|
+
# An abstract parent has no serializer for its sideloads, so the subclass applies them here.
|
|
183
|
+
if abstract_class?
|
|
184
|
+
klass.config[:sideloads].each_pair do |name, sideload|
|
|
185
|
+
klass.apply_sideload_to_serializer(name) if klass.eagerly_apply_sideload?(sideload)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
143
189
|
if defined?(::Rails) && ::Rails.env.development?
|
|
144
190
|
# Avoid adding dupe resources when re-autoloading
|
|
145
191
|
Graphiti.resources.reject! { |r| r.name == klass.name }
|
|
@@ -149,6 +195,52 @@ module Graphiti
|
|
|
149
195
|
end
|
|
150
196
|
|
|
151
197
|
class_methods do
|
|
198
|
+
# Deprecated. Both folded into filter_blanks_treated_as. Remove in 3.0.
|
|
199
|
+
def filters_accept_nil_by_default
|
|
200
|
+
filter_blanks_treated_as == :null
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def filters_accept_nil_by_default=(val)
|
|
204
|
+
self.filter_blanks_treated_as = val ? :null : :literal
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def filters_deny_empty_by_default
|
|
208
|
+
filter_blanks_treated_as == :rejected
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def filters_deny_empty_by_default=(val)
|
|
212
|
+
self.filter_blanks_treated_as = val ? :rejected : :literal
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Deprecated. Renamed to the page_ family. Remove in 3.0.
|
|
216
|
+
def default_page_size
|
|
217
|
+
page_default_size
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def default_page_size=(val)
|
|
221
|
+
self.page_default_size = val
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def max_page_size
|
|
225
|
+
page_max_size
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def max_page_size=(val)
|
|
229
|
+
self.page_max_size = val
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def cursor_paginatable
|
|
233
|
+
page_cursors
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def cursor_paginatable=(val)
|
|
237
|
+
self.page_cursors = val
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def cursor_paginatable?
|
|
241
|
+
!!page_cursors
|
|
242
|
+
end
|
|
243
|
+
|
|
152
244
|
def get_attr!(name, flag, opts = {})
|
|
153
245
|
opts[:raise_error] = true
|
|
154
246
|
get_attr(name, flag, opts)
|
|
@@ -205,14 +297,6 @@ module Graphiti
|
|
|
205
297
|
serializer_class
|
|
206
298
|
end
|
|
207
299
|
|
|
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
300
|
def config
|
|
217
301
|
@config ||=
|
|
218
302
|
{
|
|
@@ -332,5 +416,20 @@ module Graphiti
|
|
|
332
416
|
self.class.default_filters
|
|
333
417
|
end
|
|
334
418
|
end
|
|
419
|
+
|
|
420
|
+
blanks_msg = "Use `self.filter_blanks_treated_as` (:literal, :null, or :rejected)"
|
|
421
|
+
page_msg = "Use `self.page_default_size`, `self.page_max_size` and `self.page_cursors`"
|
|
422
|
+
DEPRECATOR.deprecate_methods(Configuration::ClassMethods,
|
|
423
|
+
filters_accept_nil_by_default: blanks_msg,
|
|
424
|
+
"filters_accept_nil_by_default=": blanks_msg,
|
|
425
|
+
filters_deny_empty_by_default: blanks_msg,
|
|
426
|
+
"filters_deny_empty_by_default=": blanks_msg,
|
|
427
|
+
default_page_size: page_msg,
|
|
428
|
+
"default_page_size=": page_msg,
|
|
429
|
+
max_page_size: page_msg,
|
|
430
|
+
"max_page_size=": page_msg,
|
|
431
|
+
cursor_paginatable: page_msg,
|
|
432
|
+
"cursor_paginatable=": page_msg,
|
|
433
|
+
cursor_paginatable?: page_msg)
|
|
335
434
|
end
|
|
336
435
|
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: :rejected.")
|
|
212
|
+
return :rejected 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: :null.")
|
|
217
|
+
return opts[:allow_nil] ? :null : :literal
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
blanks = opts.fetch(:blanks, filter_blanks_treated_as)
|
|
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,36 @@ 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
|
+
# true cannot express :on_demand, so it leaves an inherited on-demand mode alone.
|
|
43
|
+
def autolink=(val)
|
|
44
|
+
return if val && relationship_links == :on_demand
|
|
45
|
+
|
|
46
|
+
self.relationship_links = val ? true : false
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def autolink?
|
|
50
|
+
autolink
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Deprecated. Split into validate_requests and validate_links. Remove in 3.0.
|
|
54
|
+
def validate_endpoints
|
|
55
|
+
validate_requests && validate_links
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def validate_endpoints=(val)
|
|
59
|
+
self.validate_requests = val
|
|
60
|
+
self.validate_links = val
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def validate_endpoints?
|
|
64
|
+
validate_endpoints
|
|
65
|
+
end
|
|
66
|
+
|
|
41
67
|
def infer_endpoint
|
|
42
68
|
return unless name
|
|
43
69
|
|
|
@@ -101,4 +127,12 @@ module Graphiti
|
|
|
101
127
|
end
|
|
102
128
|
end
|
|
103
129
|
end
|
|
130
|
+
|
|
131
|
+
msg = "Use `self.relationship_links` (true, false, or :on_demand)"
|
|
132
|
+
endpoints_msg = "Use `self.validate_requests` for inbound requests, `self.validate_links` for rendered links"
|
|
133
|
+
DEPRECATOR.deprecate_methods(Links::ClassMethods,
|
|
134
|
+
autolink: msg, "autolink=": msg, autolink?: msg,
|
|
135
|
+
validate_endpoints: endpoints_msg,
|
|
136
|
+
"validate_endpoints=": endpoints_msg,
|
|
137
|
+
validate_endpoints?: endpoints_msg)
|
|
104
138
|
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
|
@@ -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
|
|
@@ -257,15 +282,21 @@ module Graphiti
|
|
|
257
282
|
end
|
|
258
283
|
end
|
|
259
284
|
|
|
285
|
+
current_attributes = current_attributes_snapshot
|
|
286
|
+
|
|
260
287
|
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|
|
|
288
|
+
self.class.global_thread_pool_executor, Thread.current.object_id, thread_storage, fiber_storage, current_attributes, *args
|
|
289
|
+
) do |thread_id, thread_storage, fiber_storage, current_attributes, *args|
|
|
263
290
|
self.class.marking_pool_thread do
|
|
264
291
|
wrap_in_rails_executor do
|
|
265
292
|
with_thread_locals(thread_storage) do
|
|
266
293
|
with_fiber_locals(fiber_storage) do
|
|
267
|
-
|
|
268
|
-
|
|
294
|
+
with_current_attributes(current_attributes) do
|
|
295
|
+
with_connection_pool_hint do
|
|
296
|
+
Graphiti.broadcast(:global_thread_pool_task_run, self.class.global_thread_pool_stats) do
|
|
297
|
+
yield(*args)
|
|
298
|
+
end
|
|
299
|
+
end
|
|
269
300
|
end
|
|
270
301
|
end
|
|
271
302
|
end
|
|
@@ -274,6 +305,45 @@ module Graphiti
|
|
|
274
305
|
end
|
|
275
306
|
end
|
|
276
307
|
|
|
308
|
+
def current_attributes_snapshot
|
|
309
|
+
return unless defined?(ActiveSupport::CurrentAttributes)
|
|
310
|
+
|
|
311
|
+
snapshot = {}
|
|
312
|
+
klasses = ActiveSupport::CurrentAttributes.subclasses
|
|
313
|
+
while (klass = klasses.shift)
|
|
314
|
+
klasses.concat(klass.subclasses)
|
|
315
|
+
attributes = klass.attributes
|
|
316
|
+
snapshot[klass] = attributes.dup if attributes.any?
|
|
317
|
+
end
|
|
318
|
+
snapshot
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Restored inside the Rails executor, whose entry hands the pool thread a
|
|
322
|
+
# fresh Current. Restoring through set scopes the values to the block.
|
|
323
|
+
def with_current_attributes(snapshot, &block)
|
|
324
|
+
return yield if snapshot.nil? || snapshot.empty?
|
|
325
|
+
|
|
326
|
+
klass, attributes = snapshot.first
|
|
327
|
+
rest = snapshot.except(klass)
|
|
328
|
+
klass.set(attributes) { with_current_attributes(rest, &block) }
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# A timeout inside a sideload task nearly always means database.yml's pool
|
|
332
|
+
# was not sized for the sideload threads, and the bare error does not say so.
|
|
333
|
+
def with_connection_pool_hint
|
|
334
|
+
yield
|
|
335
|
+
rescue => error
|
|
336
|
+
raise unless defined?(ActiveRecord::ConnectionTimeoutError) && error.is_a?(ActiveRecord::ConnectionTimeoutError)
|
|
337
|
+
|
|
338
|
+
hinted = error.exception(<<~MSG.strip)
|
|
339
|
+
#{error.message}
|
|
340
|
+
|
|
341
|
+
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.
|
|
342
|
+
MSG
|
|
343
|
+
hinted.set_backtrace(error.backtrace)
|
|
344
|
+
raise hinted
|
|
345
|
+
end
|
|
346
|
+
|
|
277
347
|
def with_thread_locals(thread_locals)
|
|
278
348
|
new_thread_locals = []
|
|
279
349
|
thread_locals.each do |key, value|
|
|
@@ -316,15 +386,19 @@ module Graphiti
|
|
|
316
386
|
|
|
317
387
|
def sideload_resource_proxies
|
|
318
388
|
@sideload_resource_proxies ||= begin
|
|
319
|
-
|
|
320
|
-
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
|
|
321
395
|
|
|
322
396
|
[].tap do |proxies|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
397
|
+
each_applicable_sideload do |name, sideload, q|
|
|
398
|
+
captured = @resolved_sideload_proxies[name]
|
|
399
|
+
if captured
|
|
400
|
+
proxies.concat(captured)
|
|
401
|
+
else
|
|
328
402
|
proxies << sideload.build_resource_proxy(results, q, parent_resource)
|
|
329
403
|
end
|
|
330
404
|
end
|
|
@@ -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] == :null
|
|
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] == :rejected
|
|
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
|