graphiti 2.0.0.beta.8 → 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/.github/workflows/ci.yml +1 -1
- data/.github/workflows/docs.yml +9 -6
- data/.github/workflows/release.yml +1 -1
- data/CHANGELOG.md +43 -0
- data/Rakefile +28 -0
- data/UPGRADING.md +1 -1
- 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 +66 -28
- data/lib/graphiti/debugger.rb +18 -6
- data/lib/graphiti/delegates/pagination.rb +2 -2
- data/lib/graphiti/error_serializers/deprecated_constants.rb +23 -0
- data/lib/graphiti/error_serializers/invalid_request.rb +6 -0
- data/lib/graphiti/errors.rb +51 -3
- data/lib/graphiti/query.rb +36 -14
- data/lib/graphiti/rails/controller.rb +26 -6
- data/lib/graphiti/rails/rake_helpers.rb +17 -0
- data/lib/graphiti/rails.rb +14 -0
- data/lib/graphiti/railtie.rb +8 -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 +115 -23
- 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/polymorphic_belongs_to.rb +11 -4
- data/lib/graphiti/sideload.rb +22 -14
- 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 +26 -16
- data/lib/graphiti/version.rb +1 -1
- data/lib/graphiti.rb +6 -1
- data/lib/tasks/graphiti.rake +4 -0
- data/package-lock.json +114 -0
- data/package.json +16 -3
- metadata +3 -2
data/lib/graphiti/scope.rb
CHANGED
|
@@ -31,6 +31,28 @@ module Graphiti
|
|
|
31
31
|
end
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
POOL_THREAD = :__graphiti_pool_thread
|
|
35
|
+
private_constant :POOL_THREAD
|
|
36
|
+
|
|
37
|
+
# A pool thread that waits on the pool deadlocks, since the task it waits for cannot start until the waiting thread frees its slot.
|
|
38
|
+
def self.resolve_synchronously?
|
|
39
|
+
!Graphiti.config.concurrency || on_pool_thread?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# TODO: move to Fiber[] once the floor is Ruby 3.2
|
|
43
|
+
def self.on_pool_thread?
|
|
44
|
+
Thread.current[POOL_THREAD] == true
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Restores rather than clears because :caller_runs may have run the task on a request thread.
|
|
48
|
+
def self.marking_pool_thread
|
|
49
|
+
previous = Thread.current[POOL_THREAD]
|
|
50
|
+
Thread.current[POOL_THREAD] = true
|
|
51
|
+
yield
|
|
52
|
+
ensure
|
|
53
|
+
Thread.current[POOL_THREAD] = previous
|
|
54
|
+
end
|
|
55
|
+
|
|
34
56
|
def initialize(object, resource, query, opts = {})
|
|
35
57
|
@object = object
|
|
36
58
|
@resource = resource
|
|
@@ -43,24 +65,24 @@ module Graphiti
|
|
|
43
65
|
end
|
|
44
66
|
|
|
45
67
|
def resolve(&blk)
|
|
46
|
-
#
|
|
47
|
-
# pre-1.8 semantics. This avoids allocating Concurrent::Promises futures,
|
|
48
|
-
# Thread/Fiber storage snapshots, and Rails executor wrappers on every
|
|
49
|
-
# request purely to drive a thread pool that is intentionally synchronous.
|
|
68
|
+
# The caller blocks on .value! either way, so concurrency only benefits parallel sideloads
|
|
50
69
|
# See https://github.com/graphiti-api/graphiti/issues/505
|
|
51
|
-
if
|
|
52
|
-
future_resolve(&blk).value!
|
|
53
|
-
else
|
|
70
|
+
if self.class.resolve_synchronously? || !applicable_sideloads?
|
|
54
71
|
sync_resolve(&blk)
|
|
72
|
+
else
|
|
73
|
+
future_resolve(&blk).value!
|
|
55
74
|
end
|
|
56
75
|
end
|
|
57
76
|
|
|
58
77
|
def resolve_sideloads(results)
|
|
59
|
-
if
|
|
60
|
-
future_resolve_sideloads(results).value!
|
|
61
|
-
else
|
|
78
|
+
if self.class.resolve_synchronously?
|
|
62
79
|
sync_resolve_sideloads(results)
|
|
80
|
+
else
|
|
81
|
+
future_resolve_sideloads(results).value!
|
|
63
82
|
end
|
|
83
|
+
|
|
84
|
+
# Never return the sideloads hash, a caller mutating it would mess up the cache key
|
|
85
|
+
nil
|
|
64
86
|
end
|
|
65
87
|
|
|
66
88
|
def future_resolve(&blk)
|
|
@@ -138,10 +160,6 @@ module Graphiti
|
|
|
138
160
|
Graphiti.config.before_sideload&.call(Graphiti.context)
|
|
139
161
|
sideload.resolve(results, sideload_query, @resource)
|
|
140
162
|
end
|
|
141
|
-
|
|
142
|
-
# Match pre-1.8 semantics: the non-concurrent resolve_sideloads returned
|
|
143
|
-
# nil (not the sideloads Hash). Callers don't rely on the return value.
|
|
144
|
-
nil
|
|
145
163
|
end
|
|
146
164
|
|
|
147
165
|
# Resolve this scope's own data: run hooks, resolve the resource, and
|
|
@@ -165,14 +183,36 @@ module Graphiti
|
|
|
165
183
|
# canonical instance. The resource class in the key keeps two resources
|
|
166
184
|
# serving the same model from sharing an instance and a serializer.
|
|
167
185
|
def deduplicate_entities!(resolved)
|
|
186
|
+
return unless deduplicable?
|
|
187
|
+
|
|
188
|
+
# Nested maps are built once, a composite key meant a new array for every record
|
|
189
|
+
by_model = @query.entity_map.compute_if_absent(@resource.class) { Concurrent::Map.new }
|
|
190
|
+
|
|
168
191
|
resolved.map! do |record|
|
|
169
192
|
next record unless record.respond_to?(:id) && !record.id.nil?
|
|
170
193
|
|
|
171
|
-
|
|
172
|
-
|
|
194
|
+
by_id = by_model.compute_if_absent(record.class) { Concurrent::Map.new }
|
|
195
|
+
by_id.compute_if_absent(record.id) { record }
|
|
173
196
|
end
|
|
174
197
|
end
|
|
175
198
|
|
|
199
|
+
# A customized sideload can load a record another path would not, so it keeps its own instances.
|
|
200
|
+
def deduplicable?
|
|
201
|
+
sideload = @opts[:sideload]
|
|
202
|
+
return true unless sideload
|
|
203
|
+
|
|
204
|
+
sideload.scope_proc.nil? &&
|
|
205
|
+
sideload.params_proc.nil? &&
|
|
206
|
+
!sideload.customized_base_scope? &&
|
|
207
|
+
sideload.primary_key == :id
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Nothing to post to the pool means the future would wrap work already done.
|
|
211
|
+
def applicable_sideloads?
|
|
212
|
+
each_applicable_sideload { return true }
|
|
213
|
+
false
|
|
214
|
+
end
|
|
215
|
+
|
|
176
216
|
def each_applicable_sideload
|
|
177
217
|
@query.sideloads.each_pair do |name, sideload_query|
|
|
178
218
|
sideload = @resource.class.sideload(name)
|
|
@@ -194,6 +234,8 @@ module Graphiti
|
|
|
194
234
|
sideload_promises << promise.flat
|
|
195
235
|
end
|
|
196
236
|
|
|
237
|
+
return sideload_promises.first if sideload_promises.one?
|
|
238
|
+
|
|
197
239
|
Concurrent::Promises.zip_futures_on(self.class.global_thread_pool_executor, *sideload_promises)
|
|
198
240
|
.rescue_on(self.class.global_thread_pool_executor) do |*reasons|
|
|
199
241
|
first_error = reasons.find { |r| r.is_a?(Exception) }
|
|
@@ -202,7 +244,10 @@ module Graphiti
|
|
|
202
244
|
end
|
|
203
245
|
|
|
204
246
|
def future_with_context(*args)
|
|
247
|
+
# TODO: we only need Fiber.storage after Ruby 3.2 is the floor
|
|
205
248
|
thread_storage = Thread.current.keys.each_with_object({}) do |key, memo|
|
|
249
|
+
next if key == POOL_THREAD
|
|
250
|
+
|
|
206
251
|
memo[key] = Thread.current[key]
|
|
207
252
|
end
|
|
208
253
|
fiber_storage =
|
|
@@ -212,14 +257,22 @@ module Graphiti
|
|
|
212
257
|
end
|
|
213
258
|
end
|
|
214
259
|
|
|
260
|
+
current_attributes = current_attributes_snapshot
|
|
261
|
+
|
|
215
262
|
Concurrent::Promises.future_on(
|
|
216
|
-
self.class.global_thread_pool_executor, Thread.current.object_id, thread_storage, fiber_storage, *args
|
|
217
|
-
) do |thread_id, thread_storage, fiber_storage, *args|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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|
|
|
265
|
+
self.class.marking_pool_thread do
|
|
266
|
+
wrap_in_rails_executor do
|
|
267
|
+
with_thread_locals(thread_storage) do
|
|
268
|
+
with_fiber_locals(fiber_storage) do
|
|
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
|
|
275
|
+
end
|
|
223
276
|
end
|
|
224
277
|
end
|
|
225
278
|
end
|
|
@@ -227,6 +280,45 @@ module Graphiti
|
|
|
227
280
|
end
|
|
228
281
|
end
|
|
229
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
|
+
|
|
230
322
|
def with_thread_locals(thread_locals)
|
|
231
323
|
new_thread_locals = []
|
|
232
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
|
|
@@ -108,10 +108,10 @@ class Graphiti::Sideload::PolymorphicBelongsTo < Graphiti::Sideload::BelongsTo
|
|
|
108
108
|
end
|
|
109
109
|
|
|
110
110
|
def resolve(parents, query, graph_parent)
|
|
111
|
-
if Graphiti.
|
|
112
|
-
future_resolve(parents, query, graph_parent).value!
|
|
113
|
-
else
|
|
111
|
+
if ::Graphiti::Scope.resolve_synchronously?
|
|
114
112
|
sync_resolve(parents, query, graph_parent)
|
|
113
|
+
else
|
|
114
|
+
future_resolve(parents, query, graph_parent).value!
|
|
115
115
|
end
|
|
116
116
|
end
|
|
117
117
|
|
|
@@ -120,7 +120,14 @@ class Graphiti::Sideload::PolymorphicBelongsTo < Graphiti::Sideload::BelongsTo
|
|
|
120
120
|
each_resolvable_group(parents, query) do |child, group, child_query|
|
|
121
121
|
promises << child.future_resolve(group, child_query, graph_parent)
|
|
122
122
|
end
|
|
123
|
-
|
|
123
|
+
return promises.first if promises.one?
|
|
124
|
+
|
|
125
|
+
executor = ::Graphiti::Scope.global_thread_pool_executor
|
|
126
|
+
Concurrent::Promises.zip_futures_on(executor, *promises)
|
|
127
|
+
.rescue_on(executor) do |*reasons|
|
|
128
|
+
first_error = reasons.find { |reason| reason.is_a?(Exception) }
|
|
129
|
+
raise first_error
|
|
130
|
+
end
|
|
124
131
|
end
|
|
125
132
|
|
|
126
133
|
private
|
data/lib/graphiti/sideload.rb
CHANGED
|
@@ -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)
|
|
@@ -280,9 +286,11 @@ module Graphiti
|
|
|
280
286
|
end
|
|
281
287
|
|
|
282
288
|
def load(parents, query, graph_parent)
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
289
|
+
if Scope.resolve_synchronously?
|
|
290
|
+
build_resource_proxy(parents, query, graph_parent).to_a
|
|
291
|
+
else
|
|
292
|
+
future_load(parents, query, graph_parent).value!
|
|
293
|
+
end
|
|
286
294
|
end
|
|
287
295
|
|
|
288
296
|
# Override in subclass
|
|
@@ -333,10 +341,10 @@ module Graphiti
|
|
|
333
341
|
end
|
|
334
342
|
|
|
335
343
|
def resolve(parents, query, graph_parent)
|
|
336
|
-
if
|
|
337
|
-
future_resolve(parents, query, graph_parent).value!
|
|
338
|
-
else
|
|
344
|
+
if Scope.resolve_synchronously?
|
|
339
345
|
sync_resolve(parents, query, graph_parent)
|
|
346
|
+
else
|
|
347
|
+
future_resolve(parents, query, graph_parent).value!
|
|
340
348
|
end
|
|
341
349
|
end
|
|
342
350
|
|
|
@@ -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,8 +57,8 @@ module Graphiti
|
|
|
55
57
|
private
|
|
56
58
|
|
|
57
59
|
def block
|
|
58
|
-
link_ref = link?
|
|
59
60
|
sideload_ref = @sideload
|
|
61
|
+
resource_class_ref = @resource_class
|
|
60
62
|
data_proc_ref = data_proc
|
|
61
63
|
self_ref = self
|
|
62
64
|
validate_link! if eagerly_validate_links?
|
|
@@ -71,7 +73,15 @@ module Graphiti
|
|
|
71
73
|
if sideload_ref.resource_ids_from_foreign_key? &&
|
|
72
74
|
!self_ref.send(:included_anywhere?, @proxy.query.include_hash, sideload_ref.name)
|
|
73
75
|
linkage always: sideload_ref.render_resource_ids? do
|
|
74
|
-
foreign_key =
|
|
76
|
+
foreign_key = begin
|
|
77
|
+
@object.public_send(sideload_ref.foreign_key)
|
|
78
|
+
rescue NoMethodError => error
|
|
79
|
+
raise unless defined?(ActiveModel::MissingAttributeError) &&
|
|
80
|
+
error.is_a?(ActiveModel::MissingAttributeError)
|
|
81
|
+
|
|
82
|
+
raise Errors::UnselectedForeignKey
|
|
83
|
+
.new(resource_class_ref, sideload_ref, @object)
|
|
84
|
+
end
|
|
75
85
|
|
|
76
86
|
unless foreign_key.nil?
|
|
77
87
|
{
|
|
@@ -84,13 +94,11 @@ module Graphiti
|
|
|
84
94
|
linkage always: sideload_ref.render_resource_ids?
|
|
85
95
|
end
|
|
86
96
|
|
|
87
|
-
if
|
|
88
|
-
|
|
89
|
-
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?)
|
|
90
99
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
end
|
|
100
|
+
link(:related) do
|
|
101
|
+
::Graphiti::Util::Link.new(sideload_ref, @object).generate
|
|
94
102
|
end
|
|
95
103
|
end
|
|
96
104
|
end
|
|
@@ -153,8 +161,8 @@ module Graphiti
|
|
|
153
161
|
end
|
|
154
162
|
|
|
155
163
|
def validate_link!
|
|
156
|
-
return unless link?
|
|
157
|
-
return unless @resource_class.
|
|
164
|
+
return unless @sideload.link? && linkable?
|
|
165
|
+
return unless @resource_class.validate_links?
|
|
158
166
|
return if @sideload.link_proc
|
|
159
167
|
|
|
160
168
|
unless Graphiti.config.context_for_endpoint
|
|
@@ -183,14 +191,16 @@ module Graphiti
|
|
|
183
191
|
self.class.validated_link_cache << cache_key
|
|
184
192
|
end
|
|
185
193
|
|
|
186
|
-
|
|
187
|
-
|
|
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)
|
|
188
197
|
|
|
189
|
-
if @sideload.
|
|
190
|
-
|
|
191
|
-
|
|
198
|
+
@linkable = if @sideload.link_proc
|
|
199
|
+
true
|
|
200
|
+
elsif @sideload.respond_to?(:children)
|
|
201
|
+
@sideload.children.values.all? { |c| !c.resource.endpoint.nil? }
|
|
192
202
|
else
|
|
193
|
-
|
|
203
|
+
!@sideload.resource.endpoint.nil?
|
|
194
204
|
end
|
|
195
205
|
end
|
|
196
206
|
end
|
data/lib/graphiti/version.rb
CHANGED
data/lib/graphiti.rb
CHANGED
|
@@ -33,7 +33,7 @@ require "jsonapi/serializable"
|
|
|
33
33
|
# either way, whichever copy wins.
|
|
34
34
|
{
|
|
35
35
|
"graphiti_spec_helpers" => 'The "graphiti_spec_helpers/rspec" require and the GraphitiSpecHelpers namespace are unchanged.',
|
|
36
|
-
"graphiti-rails" => 'Graphiti::Rails and its config.graphiti options are unchanged. Drop the "graphiti-rails" require if you have one.',
|
|
36
|
+
"graphiti-rails" => 'Graphiti::Rails and its config.graphiti options are unchanged. Drop the "graphiti-rails" require if you have one, and add `include Graphiti::Rails::Controller` to controllers serving Graphiti resources. graphiti-rails installed that on every controller automatically. See graphiti.dev/upgrading.',
|
|
37
37
|
"graphiti_errors" => "Exception handling now goes through rescue_registry. Remove `include GraphitiErrors` from your controllers — Graphiti registers its own handlers, and you can add yours with `register_exception`."
|
|
38
38
|
}.each do |gem_name, guidance|
|
|
39
39
|
next unless Gem.loaded_specs.key?(gem_name)
|
|
@@ -149,6 +149,11 @@ module Graphiti
|
|
|
149
149
|
resources.each do |r|
|
|
150
150
|
r.apply_sideloads_to_serializer
|
|
151
151
|
end
|
|
152
|
+
@setup = true
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def self.setup?
|
|
156
|
+
!!@setup
|
|
152
157
|
end
|
|
153
158
|
|
|
154
159
|
def self.cache=(val)
|
data/lib/tasks/graphiti.rake
CHANGED