graphiti 1.13.4 → 2.0.0.beta.1
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 +6 -37
- data/.standard.yml +1 -1
- data/Appraisals +0 -15
- data/CHANGELOG.md +7 -32
- data/UPGRADING.md +71 -0
- data/graphiti.gemspec +19 -1
- data/lib/graphiti/debugger.rb +6 -18
- data/lib/graphiti/errors.rb +19 -13
- data/lib/graphiti/query.rb +1 -0
- data/lib/graphiti/request_validators/validator.rb +1 -11
- data/lib/graphiti/resource/configuration.rb +0 -4
- data/lib/graphiti/resource/interface.rb +1 -21
- data/lib/graphiti/resource/persistence.rb +36 -56
- data/lib/graphiti/resource/polymorphism.rb +0 -10
- data/lib/graphiti/resource.rb +11 -2
- data/lib/graphiti/resource_proxy.rb +86 -11
- data/lib/graphiti/runner.rb +2 -1
- data/lib/graphiti/scope.rb +45 -73
- data/lib/graphiti/scoping/filter.rb +1 -3
- data/lib/graphiti/serializer.rb +1 -0
- data/lib/graphiti/sideload/polymorphic_belongs_to.rb +23 -25
- data/lib/graphiti/sideload.rb +37 -38
- data/lib/graphiti/types.rb +1 -5
- data/lib/graphiti/util/hash.rb +9 -2
- data/lib/graphiti/util/persistence.rb +39 -3
- data/lib/graphiti/version.rb +1 -1
- metadata +14 -8
- data/gemfiles/rails_5_2.gemfile +0 -18
- data/gemfiles/rails_5_2_graphiti_rails.gemfile +0 -19
data/lib/graphiti/resource.rb
CHANGED
|
@@ -123,9 +123,18 @@ module Graphiti
|
|
|
123
123
|
adapter.disassociate(parent, child, association_name, type)
|
|
124
124
|
end
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
# TODO: make foreign_key a keyword once the satellite gems are rolled in - they call these positionally
|
|
127
|
+
def assign_with_relationships(meta, attributes, relationships, caller_model = nil, foreign_key = nil, model_instance: nil)
|
|
127
128
|
persistence = Graphiti::Util::Persistence \
|
|
128
|
-
.new(self, meta, attributes, relationships, caller_model, foreign_key
|
|
129
|
+
.new(self, meta, attributes, relationships, caller_model, foreign_key,
|
|
130
|
+
assigned_model: model_instance)
|
|
131
|
+
persistence.assign
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def persist_with_relationships(meta, attributes, relationships, caller_model = nil, foreign_key = nil, assigned_model: nil)
|
|
135
|
+
persistence = Graphiti::Util::Persistence \
|
|
136
|
+
.new(self, meta, attributes, relationships, caller_model, foreign_key,
|
|
137
|
+
assigned_model: assigned_model)
|
|
129
138
|
persistence.run
|
|
130
139
|
end
|
|
131
140
|
|
|
@@ -11,6 +11,7 @@ module Graphiti
|
|
|
11
11
|
payload: nil,
|
|
12
12
|
single: false,
|
|
13
13
|
raise_on_missing: false,
|
|
14
|
+
assign_action: nil,
|
|
14
15
|
cache: nil,
|
|
15
16
|
cache_expires_in: nil,
|
|
16
17
|
cache_tag: nil
|
|
@@ -22,6 +23,7 @@ module Graphiti
|
|
|
22
23
|
@payload = payload
|
|
23
24
|
@single = single
|
|
24
25
|
@raise_on_missing = raise_on_missing
|
|
26
|
+
@assign_action = assign_action
|
|
25
27
|
@cache = cache
|
|
26
28
|
@cache_expires_in = cache_expires_in
|
|
27
29
|
@cache_tag = cache_tag
|
|
@@ -80,14 +82,11 @@ module Graphiti
|
|
|
80
82
|
Renderer.new(self, options).as_graphql
|
|
81
83
|
end
|
|
82
84
|
|
|
83
|
-
# Records supplied directly, no scope resolution
|
|
84
|
-
def data=(models)
|
|
85
|
-
@data = models
|
|
86
|
-
[@data].flatten.compact.each { |record| @resource.decorate_record(record) }
|
|
87
|
-
end
|
|
88
|
-
|
|
89
85
|
def data
|
|
90
|
-
@data
|
|
86
|
+
return @data unless @data.nil?
|
|
87
|
+
return assign_attributes(@payload.params) if @assign_action
|
|
88
|
+
|
|
89
|
+
@data = begin
|
|
91
90
|
records = @scope.resolve
|
|
92
91
|
raise Graphiti::Errors::RecordNotFound if records.empty? && raise_on_missing?
|
|
93
92
|
|
|
@@ -95,6 +94,7 @@ module Graphiti
|
|
|
95
94
|
records
|
|
96
95
|
end
|
|
97
96
|
end
|
|
97
|
+
|
|
98
98
|
alias_method :to_a, :data
|
|
99
99
|
alias_method :resolve_data, :data
|
|
100
100
|
|
|
@@ -137,18 +137,56 @@ module Graphiti
|
|
|
137
137
|
@pagination ||= Delegates::Pagination.new(self)
|
|
138
138
|
end
|
|
139
139
|
|
|
140
|
+
# Apply request params to the underlying model without saving it,
|
|
141
|
+
# Rails-style: the params are always passed explicitly, in the same
|
|
142
|
+
# request-params shape find/build accept. They are validated,
|
|
143
|
+
# deserialized, and become the payload #save will persist.
|
|
144
|
+
#
|
|
145
|
+
# Idempotent per params - calling again with params that normalize to
|
|
146
|
+
# the same payload is a no-op, so the attributes callbacks fire once.
|
|
147
|
+
# Different params re-assign onto the same model instance.
|
|
148
|
+
#
|
|
149
|
+
# Note the attributes callbacks fire here, outside any transaction
|
|
150
|
+
# opened during #save - the persistence hooks wrap only the save phase,
|
|
151
|
+
# receiving this assigned model.
|
|
152
|
+
def assign_attributes(params)
|
|
153
|
+
action = @assign_action || :update
|
|
154
|
+
params = normalized_params_copy(params)
|
|
155
|
+
add_endpoint_filter(params, action)
|
|
156
|
+
validator = ::Graphiti::RequestValidator.new(@resource, params, action)
|
|
157
|
+
validator.validate!
|
|
158
|
+
|
|
159
|
+
if @assigned_model && same_write_payload?(validator.deserialized_payload)
|
|
160
|
+
return @assigned_model
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
@payload = validator.deserialized_payload
|
|
164
|
+
@assigned_model = @data = @resource.assign_with_relationships(
|
|
165
|
+
@payload.meta(action: action),
|
|
166
|
+
@payload.attributes,
|
|
167
|
+
@payload.relationships,
|
|
168
|
+
model_instance: @assigned_model || (data if action == :update)
|
|
169
|
+
)
|
|
170
|
+
end
|
|
171
|
+
|
|
140
172
|
def save(action: :create)
|
|
141
173
|
# TODO: remove this. Only used for persisting many-to-many with AR
|
|
142
174
|
# (see activerecord adapter)
|
|
143
175
|
original = Graphiti.context[:namespace]
|
|
144
176
|
begin
|
|
145
177
|
Graphiti.context[:namespace] = action
|
|
146
|
-
|
|
178
|
+
# An assigned model can only come from #assign_attributes, which
|
|
179
|
+
# validated the payload it stored - re-validating here would run the
|
|
180
|
+
# writable guards (and their guard_model lookups) a redundant time.
|
|
181
|
+
unless @assigned_model
|
|
182
|
+
::Graphiti::RequestValidator.new(@resource, @payload.params, action).validate!
|
|
183
|
+
end
|
|
147
184
|
validator = persist {
|
|
148
185
|
@resource.persist_with_relationships \
|
|
149
186
|
@payload.meta(action: action),
|
|
150
187
|
@payload.attributes,
|
|
151
|
-
@payload.relationships
|
|
188
|
+
@payload.relationships,
|
|
189
|
+
assigned_model: @assigned_model
|
|
152
190
|
}
|
|
153
191
|
ensure
|
|
154
192
|
Graphiti.context[:namespace] = original
|
|
@@ -185,12 +223,16 @@ module Graphiti
|
|
|
185
223
|
success
|
|
186
224
|
end
|
|
187
225
|
|
|
188
|
-
|
|
226
|
+
# Rails-style: pass params to assign and save in one call, or call with
|
|
227
|
+
# no arguments to save a payload assigned earlier (via find or
|
|
228
|
+
# #assign_attributes).
|
|
229
|
+
def update(params = nil)
|
|
230
|
+
assign_attributes(params) if params
|
|
189
231
|
resolve_data
|
|
190
232
|
save(action: :update)
|
|
191
233
|
end
|
|
192
234
|
|
|
193
|
-
|
|
235
|
+
alias_method :update_attributes, :update
|
|
194
236
|
|
|
195
237
|
def include_hash
|
|
196
238
|
@include_hash ||= begin
|
|
@@ -247,6 +289,39 @@ module Graphiti
|
|
|
247
289
|
|
|
248
290
|
private
|
|
249
291
|
|
|
292
|
+
# Validation typecasts values and injects ids into the params it is
|
|
293
|
+
# given - work on a deep copy so the caller's hash stays untouched.
|
|
294
|
+
def normalized_params_copy(params)
|
|
295
|
+
if params.respond_to?(:to_unsafe_h)
|
|
296
|
+
params.to_unsafe_h.deep_symbolize_keys
|
|
297
|
+
else
|
|
298
|
+
::Graphiti::Util::Hash.deep_dup(params)
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# UpdateValidator enforces that data.id matches the endpoint's filter id.
|
|
303
|
+
# Params that came through find already carry that filter; params passed
|
|
304
|
+
# directly to #assign_attributes usually don't, so merge in the id this
|
|
305
|
+
# proxy was found with. A payload whose data.id names a different record
|
|
306
|
+
# still fails validation with ConflictRequest. Mutates the copy made by
|
|
307
|
+
# #normalized_params_copy, never caller state.
|
|
308
|
+
def add_endpoint_filter(params, action)
|
|
309
|
+
return unless action == :update
|
|
310
|
+
|
|
311
|
+
endpoint_id = @query.filters[:id]
|
|
312
|
+
return if endpoint_id.nil? || params[:filter].try(:[], :id)
|
|
313
|
+
|
|
314
|
+
params[:filter] ||= {}
|
|
315
|
+
params[:filter][:id] = endpoint_id
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Compare only the write payload (data + included) - a repeat call whose
|
|
319
|
+
# params differ in read-side keys like sort or page is still a no-op.
|
|
320
|
+
def same_write_payload?(deserialized_payload)
|
|
321
|
+
deserialized_payload.params.values_at(:data, :included) ==
|
|
322
|
+
@payload.params.values_at(:data, :included)
|
|
323
|
+
end
|
|
324
|
+
|
|
250
325
|
def persist
|
|
251
326
|
transaction_response = @resource.transaction do
|
|
252
327
|
::Graphiti::Util::TransactionHooksRecorder.record do
|
data/lib/graphiti/runner.rb
CHANGED
|
@@ -3,12 +3,12 @@ module Graphiti
|
|
|
3
3
|
attr_reader :params
|
|
4
4
|
attr_reader :deserialized_payload
|
|
5
5
|
|
|
6
|
+
# TODO: make query and action keywords once the satellite gems are rolled in - they instantiate Runner positionally
|
|
6
7
|
def initialize(resource_class, params, query = nil, action = nil)
|
|
7
8
|
@resource_class = resource_class
|
|
8
9
|
@params = params
|
|
9
10
|
@query = query
|
|
10
11
|
@action = action
|
|
11
|
-
|
|
12
12
|
validator = RequestValidator.new(jsonapi_resource, params, action)
|
|
13
13
|
validator.validate!
|
|
14
14
|
|
|
@@ -77,6 +77,7 @@ module Graphiti
|
|
|
77
77
|
payload: deserialized_payload,
|
|
78
78
|
single: opts[:single],
|
|
79
79
|
raise_on_missing: opts[:raise_on_missing],
|
|
80
|
+
assign_action: opts[:assign_action],
|
|
80
81
|
cache: opts[:cache],
|
|
81
82
|
cache_expires_in: opts[:cache_expires_in],
|
|
82
83
|
cache_tag: opts[:cache_tag]
|
data/lib/graphiti/scope.rb
CHANGED
|
@@ -31,28 +31,6 @@ 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
|
-
|
|
56
34
|
def initialize(object, resource, query, opts = {})
|
|
57
35
|
@object = object
|
|
58
36
|
@resource = resource
|
|
@@ -70,25 +48,29 @@ module Graphiti
|
|
|
70
48
|
# Thread/Fiber storage snapshots, and Rails executor wrappers on every
|
|
71
49
|
# request purely to drive a thread pool that is intentionally synchronous.
|
|
72
50
|
# See https://github.com/graphiti-api/graphiti/issues/505
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
future_resolve(&blk).value!
|
|
77
|
-
end
|
|
51
|
+
return sync_resolve(&blk) unless Graphiti.config.concurrency
|
|
52
|
+
|
|
53
|
+
future_resolve.value!
|
|
78
54
|
end
|
|
79
55
|
|
|
80
56
|
def resolve_sideloads(results)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
future_resolve_sideloads(results).value!
|
|
85
|
-
end
|
|
57
|
+
return sync_resolve_sideloads(results) unless Graphiti.config.concurrency
|
|
58
|
+
|
|
59
|
+
future_resolve_sideloads(results).value!
|
|
86
60
|
end
|
|
87
61
|
|
|
88
|
-
def future_resolve
|
|
62
|
+
def future_resolve
|
|
89
63
|
return Concurrent::Promises.fulfilled_future([], self.class.global_thread_pool_executor) if @query.zero_results?
|
|
90
64
|
|
|
91
|
-
resolved =
|
|
65
|
+
resolved = broadcast_data { |payload|
|
|
66
|
+
@object = @resource.before_resolve(@object, @query)
|
|
67
|
+
payload[:results] = @resource.resolve(@object)
|
|
68
|
+
payload[:results]
|
|
69
|
+
}
|
|
70
|
+
resolved.compact!
|
|
71
|
+
assign_serializer(resolved)
|
|
72
|
+
yield resolved if block_given?
|
|
73
|
+
@opts[:after_resolve]&.call(resolved)
|
|
92
74
|
sideloaded = @query.parents.any?
|
|
93
75
|
close_adapter = Graphiti.config.concurrency && sideloaded
|
|
94
76
|
if close_adapter
|
|
@@ -140,29 +122,12 @@ module Graphiti
|
|
|
140
122
|
|
|
141
123
|
private
|
|
142
124
|
|
|
143
|
-
|
|
125
|
+
# Synchronous counterpart to #future_resolve, used when concurrency is off.
|
|
126
|
+
# Resolves the resource and its sideloads inline without any promise
|
|
127
|
+
# machinery. See #resolve.
|
|
128
|
+
def sync_resolve
|
|
144
129
|
return [] if @query.zero_results?
|
|
145
130
|
|
|
146
|
-
resolved = resolve_primary_data(&blk)
|
|
147
|
-
sync_resolve_sideloads(resolved)
|
|
148
|
-
resolved
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
def sync_resolve_sideloads(results)
|
|
152
|
-
return if results == []
|
|
153
|
-
|
|
154
|
-
each_applicable_sideload do |sideload, sideload_query|
|
|
155
|
-
Graphiti.config.before_sideload&.call(Graphiti.context)
|
|
156
|
-
sideload.resolve(results, sideload_query, @resource)
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
# resolve_sideloads is public, and without this it would return @query.sideloads itself,
|
|
160
|
-
# where a delete would silently drop that sideload from the cache key.
|
|
161
|
-
nil
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
# Runs inline on the calling thread in both the sync and future paths.
|
|
165
|
-
def resolve_primary_data
|
|
166
131
|
resolved = broadcast_data { |payload|
|
|
167
132
|
@object = @resource.before_resolve(@object, @query)
|
|
168
133
|
payload[:results] = @resource.resolve(@object)
|
|
@@ -172,28 +137,40 @@ module Graphiti
|
|
|
172
137
|
assign_serializer(resolved)
|
|
173
138
|
yield resolved if block_given?
|
|
174
139
|
@opts[:after_resolve]&.call(resolved)
|
|
140
|
+
sync_resolve_sideloads(resolved) unless @query.sideloads.empty?
|
|
175
141
|
resolved
|
|
176
142
|
end
|
|
177
143
|
|
|
178
|
-
|
|
179
|
-
|
|
144
|
+
# Synchronous counterpart to #future_resolve_sideloads, used when
|
|
145
|
+
# concurrency is off. Resolves each sideload inline. See #resolve_sideloads.
|
|
146
|
+
def sync_resolve_sideloads(results)
|
|
147
|
+
return if results == []
|
|
148
|
+
|
|
149
|
+
@query.sideloads.each_pair do |name, q|
|
|
180
150
|
sideload = @resource.class.sideload(name)
|
|
181
151
|
next if sideload.nil? || sideload.shared_remote?
|
|
182
152
|
|
|
183
|
-
|
|
153
|
+
Graphiti.config.before_sideload&.call(Graphiti.context)
|
|
154
|
+
sideload.resolve(results, q, @resource)
|
|
184
155
|
end
|
|
156
|
+
|
|
157
|
+
# Match pre-1.8 semantics: the non-concurrent resolve_sideloads returned
|
|
158
|
+
# nil (not the sideloads Hash). Callers don't rely on the return value.
|
|
159
|
+
nil
|
|
185
160
|
end
|
|
186
161
|
|
|
187
162
|
def future_resolve_sideloads(results)
|
|
188
163
|
return Concurrent::Promises.fulfilled_future(nil, self.class.global_thread_pool_executor) if results == []
|
|
189
164
|
|
|
190
|
-
sideload_promises =
|
|
191
|
-
|
|
192
|
-
|
|
165
|
+
sideload_promises = @query.sideloads.filter_map do |name, q|
|
|
166
|
+
sideload = @resource.class.sideload(name)
|
|
167
|
+
next if sideload.nil? || sideload.shared_remote?
|
|
168
|
+
|
|
169
|
+
p = future_with_context(results, q, @resource) do |parent_results, sideload_query, parent_resource|
|
|
193
170
|
Graphiti.config.before_sideload&.call(Graphiti.context)
|
|
194
|
-
sideload.future_resolve(parent_results,
|
|
171
|
+
sideload.future_resolve(parent_results, sideload_query, parent_resource)
|
|
195
172
|
end
|
|
196
|
-
|
|
173
|
+
p.flat
|
|
197
174
|
end
|
|
198
175
|
|
|
199
176
|
Concurrent::Promises.zip_futures_on(self.class.global_thread_pool_executor, *sideload_promises)
|
|
@@ -204,10 +181,7 @@ module Graphiti
|
|
|
204
181
|
end
|
|
205
182
|
|
|
206
183
|
def future_with_context(*args)
|
|
207
|
-
# TODO: we only need Fiber.storage after Ruby 3.2 is the floor
|
|
208
184
|
thread_storage = Thread.current.keys.each_with_object({}) do |key, memo|
|
|
209
|
-
next if key == POOL_THREAD
|
|
210
|
-
|
|
211
185
|
memo[key] = Thread.current[key]
|
|
212
186
|
end
|
|
213
187
|
fiber_storage =
|
|
@@ -220,13 +194,11 @@ module Graphiti
|
|
|
220
194
|
Concurrent::Promises.future_on(
|
|
221
195
|
self.class.global_thread_pool_executor, Thread.current.object_id, thread_storage, fiber_storage, *args
|
|
222
196
|
) do |thread_id, thread_storage, fiber_storage, *args|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
yield(*args)
|
|
229
|
-
end
|
|
197
|
+
wrap_in_rails_executor do
|
|
198
|
+
with_thread_locals(thread_storage) do
|
|
199
|
+
with_fiber_locals(fiber_storage) do
|
|
200
|
+
Graphiti.broadcast(:global_thread_pool_task_run, self.class.global_thread_pool_stats) do
|
|
201
|
+
yield(*args)
|
|
230
202
|
end
|
|
231
203
|
end
|
|
232
204
|
end
|
|
@@ -209,9 +209,7 @@ module Graphiti
|
|
|
209
209
|
end
|
|
210
210
|
|
|
211
211
|
def parse_string_null(filter, value)
|
|
212
|
-
return value
|
|
213
|
-
return value.map { |item| item == "null" ? nil : item } if value.is_a?(Array)
|
|
214
|
-
return if value == "null"
|
|
212
|
+
return if value == "null" && filter[:allow_nil]
|
|
215
213
|
|
|
216
214
|
value
|
|
217
215
|
end
|
data/lib/graphiti/serializer.rb
CHANGED
|
@@ -108,42 +108,40 @@ class Graphiti::Sideload::PolymorphicBelongsTo < Graphiti::Sideload::BelongsTo
|
|
|
108
108
|
end
|
|
109
109
|
|
|
110
110
|
def resolve(parents, query, graph_parent)
|
|
111
|
-
if
|
|
112
|
-
sync_resolve(parents, query, graph_parent)
|
|
113
|
-
else
|
|
114
|
-
future_resolve(parents, query, graph_parent).value!
|
|
115
|
-
end
|
|
116
|
-
end
|
|
111
|
+
return future_resolve(parents, query, graph_parent).value! if Graphiti.config.concurrency
|
|
117
112
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
each_resolvable_group(parents, query) do |child, group, child_query|
|
|
121
|
-
promises << child.future_resolve(group, child_query, graph_parent)
|
|
122
|
-
end
|
|
123
|
-
Concurrent::Promises.zip(*promises)
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
private
|
|
113
|
+
parents.group_by(&grouper.field_name).each_pair do |group_name, group|
|
|
114
|
+
next if group_name.nil? || grouper.ignore?(group_name)
|
|
127
115
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
116
|
+
match = ->(c) { c.group_name == group_name.to_sym }
|
|
117
|
+
if (sideload = children.values.find(&match))
|
|
118
|
+
duped = remove_invalid_sideloads(sideload.resource, query)
|
|
119
|
+
sideload.resolve(group, duped, graph_parent)
|
|
120
|
+
else
|
|
121
|
+
err = ::Graphiti::Errors::PolymorphicSideloadChildNotFound
|
|
122
|
+
raise err.new(self, group_name)
|
|
123
|
+
end
|
|
131
124
|
end
|
|
132
125
|
end
|
|
133
126
|
|
|
134
|
-
def
|
|
135
|
-
parents.group_by(&grouper.field_name).
|
|
127
|
+
def future_resolve(parents, query, graph_parent)
|
|
128
|
+
promises = parents.group_by(&grouper.field_name).filter_map do |(group_name, group)|
|
|
136
129
|
next if group_name.nil? || grouper.ignore?(group_name)
|
|
137
130
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
131
|
+
match = ->(c) { c.group_name == group_name.to_sym }
|
|
132
|
+
if (sideload = children.values.find(&match))
|
|
133
|
+
duped = remove_invalid_sideloads(sideload.resource, query)
|
|
134
|
+
sideload.future_resolve(group, duped, graph_parent)
|
|
135
|
+
else
|
|
136
|
+
err = ::Graphiti::Errors::PolymorphicSideloadChildNotFound
|
|
137
|
+
raise err.new(self, group_name)
|
|
141
138
|
end
|
|
142
|
-
|
|
143
|
-
yield child, group, remove_invalid_sideloads(child.resource, query)
|
|
144
139
|
end
|
|
140
|
+
Concurrent::Promises.zip(*promises)
|
|
145
141
|
end
|
|
146
142
|
|
|
143
|
+
private
|
|
144
|
+
|
|
147
145
|
# We may be requesting a relationship that some subclasses support,
|
|
148
146
|
# but not others. Remove anything we don't support.
|
|
149
147
|
# TODO: spec to ensure this dupe logic doesn't mutate the original
|
data/lib/graphiti/sideload.rb
CHANGED
|
@@ -240,11 +240,9 @@ module Graphiti
|
|
|
240
240
|
end
|
|
241
241
|
|
|
242
242
|
def load(parents, query, graph_parent)
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
future_load(parents, query, graph_parent).value!
|
|
247
|
-
end
|
|
243
|
+
return build_resource_proxy(parents, query, graph_parent).to_a unless Graphiti.config.concurrency
|
|
244
|
+
|
|
245
|
+
future_load(parents, query, graph_parent).value!
|
|
248
246
|
end
|
|
249
247
|
|
|
250
248
|
# Override in subclass
|
|
@@ -294,19 +292,48 @@ module Graphiti
|
|
|
294
292
|
children.replace(associated) if track_associated
|
|
295
293
|
end
|
|
296
294
|
|
|
295
|
+
# Synchronous counterpart to #future_resolve, used when concurrency is off.
|
|
296
|
+
# Mirrors #future_resolve but resolves inline via the synchronous
|
|
297
|
+
# Scope#resolve / #load paths (no promises). See Scope#sync_resolve_sideloads.
|
|
297
298
|
def resolve(parents, query, graph_parent)
|
|
298
|
-
if
|
|
299
|
-
|
|
299
|
+
return future_resolve(parents, query, graph_parent).value! if Graphiti.config.concurrency
|
|
300
|
+
|
|
301
|
+
if single? && parents.length > 1
|
|
302
|
+
raise Errors::SingularSideload.new(self, parents.length)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
if self.class.scope_proc
|
|
306
|
+
sideload_scope = fire_scope(parents)
|
|
307
|
+
sideload_scope = Scope.new sideload_scope,
|
|
308
|
+
resource,
|
|
309
|
+
query,
|
|
310
|
+
parent: graph_parent,
|
|
311
|
+
sideload: self,
|
|
312
|
+
sideload_parent_length: parents.length,
|
|
313
|
+
default_paginate: false
|
|
314
|
+
sideload_scope.resolve do |sideload_results|
|
|
315
|
+
fire_assign(parents, sideload_results)
|
|
316
|
+
end
|
|
300
317
|
else
|
|
301
|
-
|
|
318
|
+
load(parents, query, graph_parent)
|
|
302
319
|
end
|
|
303
320
|
end
|
|
304
321
|
|
|
305
322
|
def future_resolve(parents, query, graph_parent)
|
|
306
|
-
|
|
323
|
+
if single? && parents.length > 1
|
|
324
|
+
raise Errors::SingularSideload.new(self, parents.length)
|
|
325
|
+
end
|
|
307
326
|
|
|
308
327
|
if self.class.scope_proc
|
|
309
|
-
|
|
328
|
+
sideload_scope = fire_scope(parents)
|
|
329
|
+
sideload_scope = Scope.new sideload_scope,
|
|
330
|
+
resource,
|
|
331
|
+
query,
|
|
332
|
+
parent: graph_parent,
|
|
333
|
+
sideload: self,
|
|
334
|
+
sideload_parent_length: parents.length,
|
|
335
|
+
default_paginate: false
|
|
336
|
+
sideload_scope.future_resolve do |sideload_results|
|
|
310
337
|
fire_assign(parents, sideload_results)
|
|
311
338
|
end
|
|
312
339
|
else
|
|
@@ -374,34 +401,6 @@ module Graphiti
|
|
|
374
401
|
|
|
375
402
|
private
|
|
376
403
|
|
|
377
|
-
def sync_resolve(parents, query, graph_parent)
|
|
378
|
-
assert_singular!(parents)
|
|
379
|
-
|
|
380
|
-
if self.class.scope_proc
|
|
381
|
-
build_sideload_scope(parents, query, graph_parent).resolve do |sideload_results|
|
|
382
|
-
fire_assign(parents, sideload_results)
|
|
383
|
-
end
|
|
384
|
-
else
|
|
385
|
-
load(parents, query, graph_parent)
|
|
386
|
-
end
|
|
387
|
-
end
|
|
388
|
-
|
|
389
|
-
def assert_singular!(parents)
|
|
390
|
-
if single? && parents.length > 1
|
|
391
|
-
raise Errors::SingularSideload.new(self, parents.length)
|
|
392
|
-
end
|
|
393
|
-
end
|
|
394
|
-
|
|
395
|
-
def build_sideload_scope(parents, query, graph_parent)
|
|
396
|
-
Scope.new fire_scope(parents),
|
|
397
|
-
resource,
|
|
398
|
-
query,
|
|
399
|
-
parent: graph_parent,
|
|
400
|
-
sideload: self,
|
|
401
|
-
sideload_parent_length: parents.length,
|
|
402
|
-
default_paginate: false
|
|
403
|
-
end
|
|
404
|
-
|
|
405
404
|
def future_load(parents, query, graph_parent)
|
|
406
405
|
proxy = build_resource_proxy(parents, query, graph_parent)
|
|
407
406
|
proxy.respond_to?(:future_resolve_data) ? proxy.future_resolve_data : Concurrent::Promises.fulfilled_future(proxy)
|
data/lib/graphiti/types.rb
CHANGED
|
@@ -82,10 +82,6 @@ module Graphiti
|
|
|
82
82
|
Dry::Types["params.hash"][input]
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
ParamArray = Dry::Types["strict.array"].constructor { |input|
|
|
86
|
-
input.is_a?(String) ? [input] : input
|
|
87
|
-
}
|
|
88
|
-
|
|
89
85
|
REQUIRED_KEYS = [:params, :read, :write, :kind, :description]
|
|
90
86
|
|
|
91
87
|
def self.map
|
|
@@ -197,7 +193,7 @@ module Graphiti
|
|
|
197
193
|
|
|
198
194
|
arrays[:"array_of_#{name.to_s.pluralize}"] = {
|
|
199
195
|
canonical_name: name,
|
|
200
|
-
params:
|
|
196
|
+
params: Dry::Types["strict.array"].of(map[:params]),
|
|
201
197
|
read: Dry::Types["strict.array"].of(map[:read]),
|
|
202
198
|
test: Dry::Types["strict.array"].of(map[:test]),
|
|
203
199
|
write: Dry::Types["strict.array"].of(map[:write]),
|
data/lib/graphiti/util/hash.rb
CHANGED
|
@@ -55,8 +55,15 @@ module Graphiti
|
|
|
55
55
|
else
|
|
56
56
|
{}.tap do |duped|
|
|
57
57
|
hash.each_pair do |key, value|
|
|
58
|
-
value =
|
|
59
|
-
|
|
58
|
+
value = if value.is_a?(Hash)
|
|
59
|
+
deep_dup(value)
|
|
60
|
+
elsif value.is_a?(Array)
|
|
61
|
+
value.map { |element| element.is_a?(Hash) ? deep_dup(element) : element }
|
|
62
|
+
elsif value&.respond_to?(:dup) && ![Symbol, Integer].include?(value.class)
|
|
63
|
+
value.dup
|
|
64
|
+
else
|
|
65
|
+
value
|
|
66
|
+
end
|
|
60
67
|
duped[key] = value
|
|
61
68
|
end
|
|
62
69
|
end
|