graphiti 2.0.0.beta.7 → 2.0.0.beta.9
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/.git-blame-ignore-revs +2 -0
- data/.github/workflows/ci.yml +56 -4
- data/.github/workflows/docs.yml +10 -7
- data/.github/workflows/notify-example-app.yml +18 -0
- data/.github/workflows/release.yml +1 -1
- data/.gitignore +2 -0
- data/.standard.yml +6 -0
- data/CHANGELOG.md +66 -0
- data/CONTRIBUTING.md +3 -1
- data/README.md +13 -206
- data/Rakefile +34 -3
- data/UPGRADING.md +2 -2
- data/graphiti.gemspec +8 -4
- data/lib/generators/graphiti/generator_mixin.rb +1 -1
- data/lib/generators/graphiti/install_generator.rb +1 -1
- data/lib/generators/graphiti/resource_generator.rb +6 -3
- data/lib/generators/graphiti/templates/application_resource.rb.erb +19 -0
- data/lib/generators/graphiti/templates/controller.rb.erb +26 -8
- data/lib/graphiti/audit/report.rb +6 -6
- data/lib/graphiti/audit.rb +3 -4
- data/lib/graphiti/configuration.rb +11 -4
- data/lib/graphiti/debugger.rb +18 -6
- data/lib/graphiti/delegates/pagination.rb +2 -2
- data/lib/graphiti/deserializer.rb +3 -3
- 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 +22 -6
- data/lib/graphiti/extensions/temp_id.rb +1 -1
- data/lib/graphiti/query.rb +5 -4
- data/lib/graphiti/rails/context.rb +2 -1
- data/lib/graphiti/rails/controller.rb +3 -0
- data/lib/graphiti/rails/debugging.rb +1 -1
- data/lib/graphiti/rails/responders.rb +2 -1
- data/lib/graphiti/rails.rb +14 -0
- data/lib/graphiti/railtie.rb +8 -0
- data/lib/graphiti/resource/configuration.rb +1 -0
- data/lib/graphiti/resource/dsl.rb +2 -2
- data/lib/graphiti/resource/links.rb +1 -1
- data/lib/graphiti/resource/persistence.rb +1 -1
- data/lib/graphiti/resource/polymorphism.rb +1 -1
- data/lib/graphiti/resource.rb +2 -2
- data/lib/graphiti/resource_proxy.rb +0 -1
- data/lib/graphiti/scope.rb +68 -21
- data/lib/graphiti/scoping/filter.rb +5 -3
- data/lib/graphiti/scoping/paginate.rb +1 -1
- data/lib/graphiti/scoping/sort.rb +1 -1
- data/lib/graphiti/sideload/has_many.rb +1 -1
- data/lib/graphiti/sideload/polymorphic_belongs_to.rb +11 -4
- data/lib/graphiti/sideload.rb +8 -6
- data/lib/graphiti/spec_helpers/errors_proxy.rb +1 -1
- data/lib/graphiti/spec_helpers/matchers.rb +1 -1
- data/lib/graphiti/types.rb +5 -1
- data/lib/graphiti/util/cache_debug.rb +3 -3
- data/lib/graphiti/util/hash.rb +1 -1
- data/lib/graphiti/util/persistence.rb +6 -6
- data/lib/graphiti/util/serializer_relationships.rb +11 -2
- data/lib/graphiti/version.rb +1 -1
- data/lib/graphiti.rb +1 -1
- data/lib/tasks/graphiti.rake +1 -1
- data/package-lock.json +114 -0
- data/package.json +16 -3
- metadata +33 -198
- data/docs/concepts/backends-and-models.md +0 -122
- data/docs/concepts/endpoints.md +0 -183
- data/docs/concepts/links.md +0 -212
- data/docs/concepts/overview.md +0 -80
- data/docs/concepts/persisting.md +0 -376
- data/docs/concepts/relationships.md +0 -627
- data/docs/concepts/resources.md +0 -677
- data/docs/getting-started/first-api.md +0 -289
- data/docs/getting-started/installation.md +0 -185
- data/docs/intro.md +0 -307
- data/docs/js/authentication.md +0 -63
- data/docs/js/ddau.md +0 -20
- data/docs/js/extra-params.md +0 -41
- data/docs/js/index.md +0 -112
- data/docs/js/installation.md +0 -120
- data/docs/js/middleware.md +0 -72
- data/docs/js/models.md +0 -202
- data/docs/js/reads.md +0 -494
- data/docs/js/state-syncing.md +0 -100
- data/docs/js/writes.md +0 -373
- data/docs/reference/vandal.md +0 -63
- data/docs/reference/why.md +0 -13
- data/docs/topics/authorization.md +0 -155
- data/docs/topics/caching.md +0 -55
- data/docs/topics/customizing-sideloads.md +0 -156
- data/docs/topics/debugging.md +0 -242
- data/docs/topics/error-handling.md +0 -210
- data/docs/topics/etags.md +0 -46
- data/docs/topics/hopping-relationships.md +0 -149
- data/docs/topics/json-attributes.md +0 -77
- data/docs/topics/openstruct-models.md +0 -50
- data/docs/topics/remote-resources.md +0 -291
- data/docs/topics/testing.md +0 -916
- data/docs/topics/without-activerecord.md +0 -324
- data/docs/tutorial/index.md +0 -58
- data/docs/tutorial/step_0.md +0 -107
- data/docs/tutorial/step_1.md +0 -199
- data/docs/tutorial/step_2.md +0 -312
- data/docs/tutorial/step_3.md +0 -142
- data/docs/tutorial/step_4.md +0 -135
- data/docs/tutorial/step_5.md +0 -69
- data/docs/tutorial/step_6.md +0 -82
- data/docs/tutorial/step_7.md +0 -205
- data/docs/tutorial/step_8.md +0 -128
- data/docs/tutorial/step_9.md +0 -171
- data/docs/upgrading.md +0 -267
- data/website/.gitignore +0 -20
- data/website/README.md +0 -43
- data/website/docusaurus.config.js +0 -197
- data/website/package-lock.json +0 -19474
- data/website/package.json +0 -46
- data/website/sidebars.js +0 -82
- data/website/src/css/custom.css +0 -85
- data/website/src/pages/markdown-page.mdx +0 -7
- data/website/static/.nojekyll +0 -0
- data/website/static/1.13/2019/03/31/graphiti-1-0.html +0 -205
- data/website/static/1.13/2019/05/08/graphiti-1-1.html +0 -212
- data/website/static/1.13/2019/05/20/graphiti-1-2.html +0 -214
- data/website/static/1.13/2019/10/14/tutorial.html +0 -198
- data/website/static/1.13/CNAME +0 -1
- data/website/static/1.13/README.md +0 -16
- data/website/static/1.13/assets/css/syntax.css +0 -60
- data/website/static/1.13/assets/favicons/android-chrome-192x192.png +0 -0
- data/website/static/1.13/assets/favicons/android-chrome-256x256.png +0 -0
- data/website/static/1.13/assets/favicons/apple-touch-icon.png +0 -0
- data/website/static/1.13/assets/favicons/browserconfig.xml +0 -9
- data/website/static/1.13/assets/favicons/favicon-16x16.png +0 -0
- data/website/static/1.13/assets/favicons/favicon-32x32.png +0 -0
- data/website/static/1.13/assets/favicons/favicon.ico +0 -0
- data/website/static/1.13/assets/favicons/mstile-150x150.png +0 -0
- data/website/static/1.13/assets/favicons/safari-pinned-tab.svg +0 -1
- data/website/static/1.13/assets/favicons/site.webmanifest +0 -19
- data/website/static/1.13/assets/img/backend.gif +0 -0
- data/website/static/1.13/assets/img/conformity.png +0 -0
- data/website/static/1.13/assets/img/error_payload.png +0 -0
- data/website/static/1.13/assets/img/fancy-cushion.png +0 -0
- data/website/static/1.13/assets/img/gh.png +0 -0
- data/website/static/1.13/assets/img/home-bg.jpg +0 -0
- data/website/static/1.13/assets/img/lifecycle.gif +0 -0
- data/website/static/1.13/assets/img/logo-500.png +0 -0
- data/website/static/1.13/assets/img/logo.png +0 -0
- data/website/static/1.13/assets/img/love-graffiti.jpg +0 -0
- data/website/static/1.13/assets/img/meta_total_count.png +0 -0
- data/website/static/1.13/assets/img/persist.jpg +0 -0
- data/website/static/1.13/assets/img/resource.gif +0 -0
- data/website/static/1.13/assets/img/rest-graffiti.jpg +0 -0
- data/website/static/1.13/assets/img/rest1.gif +0 -0
- data/website/static/1.13/assets/img/rest2.gif +0 -0
- data/website/static/1.13/assets/img/rest3.gif +0 -0
- data/website/static/1.13/assets/img/rethink-rest-graffiti.jpg +0 -0
- data/website/static/1.13/assets/img/sunrise.png +0 -0
- data/website/static/1.13/assets/img/why.png +0 -0
- data/website/static/1.13/assets/js/highlight.pack.js +0 -2
- data/website/static/1.13/assets/main.css +0 -15518
- data/website/static/1.13/assets/main.css.map +0 -1
- data/website/static/1.13/bin/bundle +0 -109
- data/website/static/1.13/bin/jekyll +0 -27
- data/website/static/1.13/bin/kramdown +0 -27
- data/website/static/1.13/bin/listen +0 -27
- data/website/static/1.13/bin/rake +0 -27
- data/website/static/1.13/bin/rougify +0 -27
- data/website/static/1.13/bin/safe_yaml +0 -27
- data/website/static/1.13/bin/sass +0 -27
- data/website/static/1.13/bin/sass-convert +0 -27
- data/website/static/1.13/bin/scss +0 -27
- data/website/static/1.13/blog.html +0 -259
- data/website/static/1.13/cheatsheet.html +0 -316
- data/website/static/1.13/cookbooks/authorization.md +0 -0
- data/website/static/1.13/cookbooks/caching.md +0 -0
- data/website/static/1.13/cookbooks/customizing-sideloads.html +0 -325
- data/website/static/1.13/cookbooks/etags.md +0 -0
- data/website/static/1.13/cookbooks/hopping-relationships.html +0 -324
- data/website/static/1.13/cookbooks/json_attributes.md +0 -0
- data/website/static/1.13/cookbooks/openstruct-models.md +0 -0
- data/website/static/1.13/cookbooks/remote-resources.md +0 -0
- data/website/static/1.13/cookbooks/without-activerecord.html +0 -510
- data/website/static/1.13/features.html +0 -249
- data/website/static/1.13/feed.xml +0 -106
- data/website/static/1.13/guides/concepts/backends-and-models.html +0 -467
- data/website/static/1.13/guides/concepts/debugging.html +0 -440
- data/website/static/1.13/guides/concepts/endpoints.html +0 -432
- data/website/static/1.13/guides/concepts/error-handling.html +0 -396
- data/website/static/1.13/guides/concepts/links.html +0 -501
- data/website/static/1.13/guides/concepts/remote-resources.html +0 -536
- data/website/static/1.13/guides/concepts/resources.html +0 -2176
- data/website/static/1.13/guides/concepts/testing.html +0 -1469
- data/website/static/1.13/guides/getting-started/installation.html +0 -420
- data/website/static/1.13/guides/graphiti-rails-migration.html +0 -242
- data/website/static/1.13/guides/index.html +0 -269
- data/website/static/1.13/guides/overview.html +0 -325
- data/website/static/1.13/guides/upgrading-2-0.html +0 -193
- data/website/static/1.13/guides/upgrading.html +0 -314
- data/website/static/1.13/guides/vandal.html +0 -282
- data/website/static/1.13/guides/why.html +0 -1121
- data/website/static/1.13/index.html +0 -72
- data/website/static/1.13/js/authentication.html +0 -295
- data/website/static/1.13/js/ddau.html +0 -238
- data/website/static/1.13/js/extra-params.html +0 -270
- data/website/static/1.13/js/index.html +0 -321
- data/website/static/1.13/js/installation.html +0 -637
- data/website/static/1.13/js/introduction.html +0 -257
- data/website/static/1.13/js/middleware.html +0 -318
- data/website/static/1.13/js/reads/fieldsets.html +0 -271
- data/website/static/1.13/js/reads/filtering.html +0 -289
- data/website/static/1.13/js/reads/includes.html +0 -260
- data/website/static/1.13/js/reads/index.html +0 -497
- data/website/static/1.13/js/reads/nested-queries.html +0 -353
- data/website/static/1.13/js/reads/pagination.html +0 -260
- data/website/static/1.13/js/reads/sorting.html +0 -265
- data/website/static/1.13/js/reads/statistics.html +0 -289
- data/website/static/1.13/js/state-syncing.html +0 -340
- data/website/static/1.13/js/writes/deferred.html +0 -296
- data/website/static/1.13/js/writes/dirty-tracking.html +0 -399
- data/website/static/1.13/js/writes/index.html +0 -391
- data/website/static/1.13/js/writes/nested.html +0 -330
- data/website/static/1.13/js/writes/validations.html +0 -272
- data/website/static/1.13/quickstart.html +0 -660
- data/website/static/1.13/template +0 -161
- data/website/static/1.13/tutorial/index.html +0 -250
- data/website/static/1.13/tutorial/step_0.html +0 -292
- data/website/static/1.13/tutorial/step_1.html +0 -517
- data/website/static/1.13/tutorial/step_2.html +0 -481
- data/website/static/1.13/tutorial/step_3.html +0 -323
- data/website/static/1.13/tutorial/step_4.html +0 -318
- data/website/static/1.13/tutorial/step_5.html +0 -265
- data/website/static/1.13/tutorial/step_6.html +0 -276
- data/website/static/1.13/tutorial/step_7.html +0 -390
- data/website/static/1.13/tutorial/step_8.html +0 -316
- data/website/static/1.13/tutorial/step_9.html +0 -365
- data/website/static/CNAME +0 -1
- data/website/static/assets/img/error_payload.png +0 -0
- data/website/static/assets/img/legacy/legacy-0378a3bb39.png +0 -0
- data/website/static/assets/img/legacy/legacy-05bbd3e5fd.png +0 -0
- data/website/static/assets/img/legacy/legacy-07aa104495.png +0 -0
- data/website/static/assets/img/legacy/legacy-0c75a16b3a.gif +0 -0
- data/website/static/assets/img/legacy/legacy-3076df6209.png +0 -0
- data/website/static/assets/img/legacy/legacy-7f6889bc89.png +0 -0
- data/website/static/assets/img/legacy/legacy-a2cc4363c3.png +0 -0
- data/website/static/assets/img/legacy/legacy-f67cfa89ab.png +0 -0
- data/website/static/assets/img/meta_total_count.png +0 -0
- data/website/static/img/docusaurus-social-card.jpg +0 -0
- data/website/static/img/docusaurus.png +0 -0
- data/website/static/img/favicon.ico +0 -0
- data/website/static/img/logo.png +0 -0
- data/website/static/img/logo.svg +0 -1
- data/website/static/img/undraw_docusaurus_mountain.svg +0 -171
- data/website/static/img/undraw_docusaurus_react.svg +0 -170
- data/website/static/img/undraw_docusaurus_tree.svg +0 -40
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 =
|
|
@@ -215,11 +260,13 @@ module Graphiti
|
|
|
215
260
|
Concurrent::Promises.future_on(
|
|
216
261
|
self.class.global_thread_pool_executor, Thread.current.object_id, thread_storage, fiber_storage, *args
|
|
217
262
|
) do |thread_id, thread_storage, fiber_storage, *args|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
263
|
+
self.class.marking_pool_thread do
|
|
264
|
+
wrap_in_rails_executor do
|
|
265
|
+
with_thread_locals(thread_storage) do
|
|
266
|
+
with_fiber_locals(fiber_storage) do
|
|
267
|
+
Graphiti.broadcast(:global_thread_pool_task_run, self.class.global_thread_pool_stats) do
|
|
268
|
+
yield(*args)
|
|
269
|
+
end
|
|
223
270
|
end
|
|
224
271
|
end
|
|
225
272
|
end
|
|
@@ -82,7 +82,7 @@ module Graphiti
|
|
|
82
82
|
if is_array
|
|
83
83
|
@resource.typecast(name, value, :filterable)
|
|
84
84
|
else
|
|
85
|
-
value = value.nil? || value.is_a?(Hash) ? [value] : Array(value)
|
|
85
|
+
value = (value.nil? || value.is_a?(Hash)) ? [value] : Array(value)
|
|
86
86
|
value.map { |v| @resource.typecast(name, v, :filterable) }
|
|
87
87
|
end
|
|
88
88
|
end
|
|
@@ -100,7 +100,7 @@ module Graphiti
|
|
|
100
100
|
|
|
101
101
|
param_value.map do |operator, value|
|
|
102
102
|
if type == :hash
|
|
103
|
-
value, operator =
|
|
103
|
+
value, operator =
|
|
104
104
|
parse_hash_value(filter, param_value, value, operator)
|
|
105
105
|
end
|
|
106
106
|
|
|
@@ -209,7 +209,9 @@ module Graphiti
|
|
|
209
209
|
end
|
|
210
210
|
|
|
211
211
|
def parse_string_null(filter, value)
|
|
212
|
-
return
|
|
212
|
+
return value unless filter[:allow_nil]
|
|
213
|
+
return value.map { |item| (item == "null") ? nil : item } if value.is_a?(Array)
|
|
214
|
+
return if value == "null"
|
|
213
215
|
|
|
214
216
|
value
|
|
215
217
|
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
|
@@ -280,9 +280,11 @@ module Graphiti
|
|
|
280
280
|
end
|
|
281
281
|
|
|
282
282
|
def load(parents, query, graph_parent)
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
283
|
+
if Scope.resolve_synchronously?
|
|
284
|
+
build_resource_proxy(parents, query, graph_parent).to_a
|
|
285
|
+
else
|
|
286
|
+
future_load(parents, query, graph_parent).value!
|
|
287
|
+
end
|
|
286
288
|
end
|
|
287
289
|
|
|
288
290
|
# Override in subclass
|
|
@@ -333,10 +335,10 @@ module Graphiti
|
|
|
333
335
|
end
|
|
334
336
|
|
|
335
337
|
def resolve(parents, query, graph_parent)
|
|
336
|
-
if
|
|
337
|
-
future_resolve(parents, query, graph_parent).value!
|
|
338
|
-
else
|
|
338
|
+
if Scope.resolve_synchronously?
|
|
339
339
|
sync_resolve(parents, query, graph_parent)
|
|
340
|
+
else
|
|
341
|
+
future_resolve(parents, query, graph_parent).value!
|
|
340
342
|
end
|
|
341
343
|
end
|
|
342
344
|
|
|
@@ -62,7 +62,7 @@ module Graphiti
|
|
|
62
62
|
|
|
63
63
|
def method_missing(id, *args, &blk)
|
|
64
64
|
matches = @errors.select { |e| e.attribute.to_sym == id }
|
|
65
|
-
matches.length == 1 ? matches[0] : matches
|
|
65
|
+
(matches.length == 1) ? matches[0] : matches
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
# Any name is answered: an attribute with no matching error yields an empty
|
|
@@ -73,7 +73,7 @@ module Graphiti
|
|
|
73
73
|
attr_reader :target, :opts, :resource
|
|
74
74
|
|
|
75
75
|
def assert_opt(opt)
|
|
76
|
-
asserted_opt = opt == :resource ? :resource_class : opt
|
|
76
|
+
asserted_opt = (opt == :resource) ? :resource_class : opt
|
|
77
77
|
return true if config.send(asserted_opt) == opts[opt]
|
|
78
78
|
|
|
79
79
|
@opt_failures << opt_failure_message(opt, opts[opt], config.send(asserted_opt))
|
data/lib/graphiti/types.rb
CHANGED
|
@@ -82,6 +82,10 @@ 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
|
+
|
|
85
89
|
REQUIRED_KEYS = [:params, :read, :write, :kind, :description]
|
|
86
90
|
|
|
87
91
|
def self.map
|
|
@@ -193,7 +197,7 @@ module Graphiti
|
|
|
193
197
|
|
|
194
198
|
arrays[:"array_of_#{name.to_s.pluralize}"] = {
|
|
195
199
|
canonical_name: name,
|
|
196
|
-
params:
|
|
200
|
+
params: ParamArray.of(map[:params]),
|
|
197
201
|
read: Dry::Types["strict.array"].of(map[:read]),
|
|
198
202
|
test: Dry::Types["strict.array"].of(map[:test]),
|
|
199
203
|
write: Dry::Types["strict.array"].of(map[:write]),
|
|
@@ -28,7 +28,7 @@ module Graphiti
|
|
|
28
28
|
expires_in: proxy.cache_expires_in,
|
|
29
29
|
etag: proxy.etag,
|
|
30
30
|
miss_count: last_version[:miss_count].to_i + (changed_key? ? 1 : 0),
|
|
31
|
-
hit_count: last_version[:hit_count].to_i + (!changed_key? && !new_key? ? 1 : 0),
|
|
31
|
+
hit_count: last_version[:hit_count].to_i + ((!changed_key? && !new_key?) ? 1 : 0),
|
|
32
32
|
request_count: last_version[:request_count].to_i + (last_version.present? ? 1 : 0)
|
|
33
33
|
}
|
|
34
34
|
end
|
|
@@ -76,8 +76,8 @@ module Graphiti
|
|
|
76
76
|
end
|
|
77
77
|
|
|
78
78
|
def changes
|
|
79
|
-
sub_keys_old = last_version[:cache_key]&.scan(/\w+\/query-[a-z0-9-]+\/args-[a-z0-9-]+/).to_a
|
|
80
|
-
sub_keys_new = current_version[:cache_key]&.scan(/\w+\/query-[a-z0-9-]+\/args-[a-z0-9-]+/).to_a
|
|
79
|
+
sub_keys_old = last_version[:cache_key]&.scan(/\w+\/query-[a-z0-9-]+\/args-[a-z0-9-]+/).to_a
|
|
80
|
+
sub_keys_new = current_version[:cache_key]&.scan(/\w+\/query-[a-z0-9-]+\/args-[a-z0-9-]+/).to_a
|
|
81
81
|
|
|
82
82
|
[sub_keys_old, sub_keys_new]
|
|
83
83
|
end
|
data/lib/graphiti/util/hash.rb
CHANGED
|
@@ -43,7 +43,7 @@ module Graphiti
|
|
|
43
43
|
# @return [Hash] the merged hash
|
|
44
44
|
# @api private
|
|
45
45
|
def self.deep_merge!(hash, other)
|
|
46
|
-
merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
|
|
46
|
+
merger = proc { |key, v1, v2| (Hash === v1 && Hash === v2) ? v1.merge(v2, &merger) : v2 }
|
|
47
47
|
hash.merge!(other, &merger)
|
|
48
48
|
end
|
|
49
49
|
|
|
@@ -156,12 +156,12 @@ class Graphiti::Util::Persistence
|
|
|
156
156
|
|
|
157
157
|
def persist_object(method, attributes)
|
|
158
158
|
case method
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
159
|
+
when :destroy
|
|
160
|
+
call_resource_method(:destroy, attributes[:id], @caller_model)
|
|
161
|
+
when :update, nil, :disassociate
|
|
162
|
+
call_resource_method(:update, attributes, @caller_model)
|
|
163
|
+
else
|
|
164
|
+
call_resource_method(:create, attributes, @caller_model)
|
|
165
165
|
end
|
|
166
166
|
end
|
|
167
167
|
|
|
@@ -57,6 +57,7 @@ module Graphiti
|
|
|
57
57
|
def block
|
|
58
58
|
link_ref = link?
|
|
59
59
|
sideload_ref = @sideload
|
|
60
|
+
resource_class_ref = @resource_class
|
|
60
61
|
data_proc_ref = data_proc
|
|
61
62
|
self_ref = self
|
|
62
63
|
validate_link! if eagerly_validate_links?
|
|
@@ -71,7 +72,15 @@ module Graphiti
|
|
|
71
72
|
if sideload_ref.resource_ids_from_foreign_key? &&
|
|
72
73
|
!self_ref.send(:included_anywhere?, @proxy.query.include_hash, sideload_ref.name)
|
|
73
74
|
linkage always: sideload_ref.render_resource_ids? do
|
|
74
|
-
foreign_key =
|
|
75
|
+
foreign_key = begin
|
|
76
|
+
@object.public_send(sideload_ref.foreign_key)
|
|
77
|
+
rescue NoMethodError => error
|
|
78
|
+
raise unless defined?(ActiveModel::MissingAttributeError) &&
|
|
79
|
+
error.is_a?(ActiveModel::MissingAttributeError)
|
|
80
|
+
|
|
81
|
+
raise Errors::UnselectedForeignKey
|
|
82
|
+
.new(resource_class_ref, sideload_ref, @object)
|
|
83
|
+
end
|
|
75
84
|
|
|
76
85
|
unless foreign_key.nil?
|
|
77
86
|
{
|
|
@@ -173,7 +182,7 @@ module Graphiti
|
|
|
173
182
|
def validate_link_for_sideload!(sideload)
|
|
174
183
|
return if sideload.resource.remote?
|
|
175
184
|
|
|
176
|
-
action = sideload.type == :belongs_to ? :show : :index
|
|
185
|
+
action = (sideload.type == :belongs_to) ? :show : :index
|
|
177
186
|
cache_key = :"#{@sideload.object_id}-#{action}"
|
|
178
187
|
return if self.class.validated_link_cache.include?(cache_key)
|
|
179
188
|
prc = Graphiti.config.context_for_endpoint
|
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)
|
data/lib/tasks/graphiti.rake
CHANGED
data/package-lock.json
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"devDependencies": {
|
|
12
12
|
"@semantic-release/changelog": "^7.0.0",
|
|
13
|
+
"@semantic-release/exec": "^7.1.0",
|
|
13
14
|
"@semantic-release/git": "^11.0.1",
|
|
14
15
|
"semantic-release": "^25.0.8",
|
|
15
16
|
"semantic-release-rubygem": "^1.2.0"
|
|
@@ -394,6 +395,119 @@
|
|
|
394
395
|
"node": ">=18"
|
|
395
396
|
}
|
|
396
397
|
},
|
|
398
|
+
"node_modules/@semantic-release/exec": {
|
|
399
|
+
"version": "7.1.0",
|
|
400
|
+
"resolved": "https://registry.npmjs.org/@semantic-release/exec/-/exec-7.1.0.tgz",
|
|
401
|
+
"integrity": "sha512-4ycZ2atgEUutspPZ2hxO6z8JoQt4+y/kkHvfZ1cZxgl9WKJId1xPj+UadwInj+gMn2Gsv+fLnbrZ4s+6tK2TFQ==",
|
|
402
|
+
"dev": true,
|
|
403
|
+
"license": "MIT",
|
|
404
|
+
"dependencies": {
|
|
405
|
+
"@semantic-release/error": "^4.0.0",
|
|
406
|
+
"aggregate-error": "^3.0.0",
|
|
407
|
+
"debug": "^4.0.0",
|
|
408
|
+
"execa": "^9.0.0",
|
|
409
|
+
"lodash-es": "^4.17.21",
|
|
410
|
+
"parse-json": "^8.0.0"
|
|
411
|
+
},
|
|
412
|
+
"engines": {
|
|
413
|
+
"node": ">=20.8.1"
|
|
414
|
+
},
|
|
415
|
+
"peerDependencies": {
|
|
416
|
+
"semantic-release": ">=24.1.0"
|
|
417
|
+
}
|
|
418
|
+
},
|
|
419
|
+
"node_modules/@semantic-release/exec/node_modules/aggregate-error": {
|
|
420
|
+
"version": "3.1.0",
|
|
421
|
+
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
|
|
422
|
+
"integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
|
|
423
|
+
"dev": true,
|
|
424
|
+
"license": "MIT",
|
|
425
|
+
"dependencies": {
|
|
426
|
+
"clean-stack": "^2.0.0",
|
|
427
|
+
"indent-string": "^4.0.0"
|
|
428
|
+
},
|
|
429
|
+
"engines": {
|
|
430
|
+
"node": ">=8"
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
"node_modules/@semantic-release/exec/node_modules/clean-stack": {
|
|
434
|
+
"version": "2.2.0",
|
|
435
|
+
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
|
|
436
|
+
"integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
|
|
437
|
+
"dev": true,
|
|
438
|
+
"license": "MIT",
|
|
439
|
+
"engines": {
|
|
440
|
+
"node": ">=6"
|
|
441
|
+
}
|
|
442
|
+
},
|
|
443
|
+
"node_modules/@semantic-release/exec/node_modules/execa": {
|
|
444
|
+
"version": "9.6.1",
|
|
445
|
+
"resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz",
|
|
446
|
+
"integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==",
|
|
447
|
+
"dev": true,
|
|
448
|
+
"license": "MIT",
|
|
449
|
+
"dependencies": {
|
|
450
|
+
"@sindresorhus/merge-streams": "^4.0.0",
|
|
451
|
+
"cross-spawn": "^7.0.6",
|
|
452
|
+
"figures": "^6.1.0",
|
|
453
|
+
"get-stream": "^9.0.0",
|
|
454
|
+
"human-signals": "^8.0.1",
|
|
455
|
+
"is-plain-obj": "^4.1.0",
|
|
456
|
+
"is-stream": "^4.0.1",
|
|
457
|
+
"npm-run-path": "^6.0.0",
|
|
458
|
+
"pretty-ms": "^9.2.0",
|
|
459
|
+
"signal-exit": "^4.1.0",
|
|
460
|
+
"strip-final-newline": "^4.0.0",
|
|
461
|
+
"yoctocolors": "^2.1.1"
|
|
462
|
+
},
|
|
463
|
+
"engines": {
|
|
464
|
+
"node": "^18.19.0 || >=20.5.0"
|
|
465
|
+
},
|
|
466
|
+
"funding": {
|
|
467
|
+
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
"node_modules/@semantic-release/exec/node_modules/indent-string": {
|
|
471
|
+
"version": "4.0.0",
|
|
472
|
+
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
|
|
473
|
+
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
|
|
474
|
+
"dev": true,
|
|
475
|
+
"license": "MIT",
|
|
476
|
+
"engines": {
|
|
477
|
+
"node": ">=8"
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
"node_modules/@semantic-release/exec/node_modules/parse-json": {
|
|
481
|
+
"version": "8.3.0",
|
|
482
|
+
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz",
|
|
483
|
+
"integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==",
|
|
484
|
+
"dev": true,
|
|
485
|
+
"license": "MIT",
|
|
486
|
+
"dependencies": {
|
|
487
|
+
"@babel/code-frame": "^7.26.2",
|
|
488
|
+
"index-to-position": "^1.1.0",
|
|
489
|
+
"type-fest": "^4.39.1"
|
|
490
|
+
},
|
|
491
|
+
"engines": {
|
|
492
|
+
"node": ">=18"
|
|
493
|
+
},
|
|
494
|
+
"funding": {
|
|
495
|
+
"url": "https://github.com/sponsors/sindresorhus"
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
"node_modules/@semantic-release/exec/node_modules/type-fest": {
|
|
499
|
+
"version": "4.41.0",
|
|
500
|
+
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
|
|
501
|
+
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
|
|
502
|
+
"dev": true,
|
|
503
|
+
"license": "(MIT OR CC0-1.0)",
|
|
504
|
+
"engines": {
|
|
505
|
+
"node": ">=16"
|
|
506
|
+
},
|
|
507
|
+
"funding": {
|
|
508
|
+
"url": "https://github.com/sponsors/sindresorhus"
|
|
509
|
+
}
|
|
510
|
+
},
|
|
397
511
|
"node_modules/@semantic-release/git": {
|
|
398
512
|
"version": "11.0.1",
|
|
399
513
|
"resolved": "https://registry.npmjs.org/@semantic-release/git/-/git-11.0.1.tgz",
|
data/package.json
CHANGED
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"semantic-release-rubygem": "^1.2.0",
|
|
19
19
|
"semantic-release": "^25.0.8",
|
|
20
20
|
"@semantic-release/changelog": "^7.0.0",
|
|
21
|
-
"@semantic-release/git": "^11.0.1"
|
|
21
|
+
"@semantic-release/git": "^11.0.1",
|
|
22
|
+
"@semantic-release/exec": "^7.1.0"
|
|
22
23
|
},
|
|
23
24
|
"release": {
|
|
24
25
|
"branches": [
|
|
@@ -132,13 +133,25 @@
|
|
|
132
133
|
}
|
|
133
134
|
],
|
|
134
135
|
"semantic-release-rubygem",
|
|
135
|
-
|
|
136
|
+
[
|
|
137
|
+
"@semantic-release/github",
|
|
138
|
+
{
|
|
139
|
+
"successCommentCondition": "<% return !branch.prerelease; %>"
|
|
140
|
+
}
|
|
141
|
+
],
|
|
142
|
+
[
|
|
143
|
+
"@semantic-release/exec",
|
|
144
|
+
{
|
|
145
|
+
"prepareCmd": "bundle exec ruby spec/performance/measure_releases.rb --pending v${nextRelease.version}"
|
|
146
|
+
}
|
|
147
|
+
],
|
|
136
148
|
[
|
|
137
149
|
"@semantic-release/git",
|
|
138
150
|
{
|
|
139
151
|
"assets": [
|
|
140
152
|
"CHANGELOG.md",
|
|
141
|
-
"lib/graphiti/version.rb"
|
|
153
|
+
"lib/graphiti/version.rb",
|
|
154
|
+
"spec/performance/performance_history.tsv"
|
|
142
155
|
],
|
|
143
156
|
"message": "${nextRelease.version} CHANGELOG [skip ci]\n\n${nextRelease.notes}"
|
|
144
157
|
}
|