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.
Files changed (251) hide show
  1. checksums.yaml +4 -4
  2. data/.git-blame-ignore-revs +2 -0
  3. data/.github/workflows/ci.yml +56 -4
  4. data/.github/workflows/docs.yml +10 -7
  5. data/.github/workflows/notify-example-app.yml +18 -0
  6. data/.github/workflows/release.yml +1 -1
  7. data/.gitignore +2 -0
  8. data/.standard.yml +6 -0
  9. data/CHANGELOG.md +66 -0
  10. data/CONTRIBUTING.md +3 -1
  11. data/README.md +13 -206
  12. data/Rakefile +34 -3
  13. data/UPGRADING.md +2 -2
  14. data/graphiti.gemspec +8 -4
  15. data/lib/generators/graphiti/generator_mixin.rb +1 -1
  16. data/lib/generators/graphiti/install_generator.rb +1 -1
  17. data/lib/generators/graphiti/resource_generator.rb +6 -3
  18. data/lib/generators/graphiti/templates/application_resource.rb.erb +19 -0
  19. data/lib/generators/graphiti/templates/controller.rb.erb +26 -8
  20. data/lib/graphiti/audit/report.rb +6 -6
  21. data/lib/graphiti/audit.rb +3 -4
  22. data/lib/graphiti/configuration.rb +11 -4
  23. data/lib/graphiti/debugger.rb +18 -6
  24. data/lib/graphiti/delegates/pagination.rb +2 -2
  25. data/lib/graphiti/deserializer.rb +3 -3
  26. data/lib/graphiti/error_serializers/deprecated_constants.rb +23 -0
  27. data/lib/graphiti/error_serializers/invalid_request.rb +6 -0
  28. data/lib/graphiti/errors.rb +22 -6
  29. data/lib/graphiti/extensions/temp_id.rb +1 -1
  30. data/lib/graphiti/query.rb +5 -4
  31. data/lib/graphiti/rails/context.rb +2 -1
  32. data/lib/graphiti/rails/controller.rb +3 -0
  33. data/lib/graphiti/rails/debugging.rb +1 -1
  34. data/lib/graphiti/rails/responders.rb +2 -1
  35. data/lib/graphiti/rails.rb +14 -0
  36. data/lib/graphiti/railtie.rb +8 -0
  37. data/lib/graphiti/resource/configuration.rb +1 -0
  38. data/lib/graphiti/resource/dsl.rb +2 -2
  39. data/lib/graphiti/resource/links.rb +1 -1
  40. data/lib/graphiti/resource/persistence.rb +1 -1
  41. data/lib/graphiti/resource/polymorphism.rb +1 -1
  42. data/lib/graphiti/resource.rb +2 -2
  43. data/lib/graphiti/resource_proxy.rb +0 -1
  44. data/lib/graphiti/scope.rb +68 -21
  45. data/lib/graphiti/scoping/filter.rb +5 -3
  46. data/lib/graphiti/scoping/paginate.rb +1 -1
  47. data/lib/graphiti/scoping/sort.rb +1 -1
  48. data/lib/graphiti/sideload/has_many.rb +1 -1
  49. data/lib/graphiti/sideload/polymorphic_belongs_to.rb +11 -4
  50. data/lib/graphiti/sideload.rb +8 -6
  51. data/lib/graphiti/spec_helpers/errors_proxy.rb +1 -1
  52. data/lib/graphiti/spec_helpers/matchers.rb +1 -1
  53. data/lib/graphiti/types.rb +5 -1
  54. data/lib/graphiti/util/cache_debug.rb +3 -3
  55. data/lib/graphiti/util/hash.rb +1 -1
  56. data/lib/graphiti/util/persistence.rb +6 -6
  57. data/lib/graphiti/util/serializer_relationships.rb +11 -2
  58. data/lib/graphiti/version.rb +1 -1
  59. data/lib/graphiti.rb +1 -1
  60. data/lib/tasks/graphiti.rake +1 -1
  61. data/package-lock.json +114 -0
  62. data/package.json +16 -3
  63. metadata +33 -198
  64. data/docs/concepts/backends-and-models.md +0 -122
  65. data/docs/concepts/endpoints.md +0 -183
  66. data/docs/concepts/links.md +0 -212
  67. data/docs/concepts/overview.md +0 -80
  68. data/docs/concepts/persisting.md +0 -376
  69. data/docs/concepts/relationships.md +0 -627
  70. data/docs/concepts/resources.md +0 -677
  71. data/docs/getting-started/first-api.md +0 -289
  72. data/docs/getting-started/installation.md +0 -185
  73. data/docs/intro.md +0 -307
  74. data/docs/js/authentication.md +0 -63
  75. data/docs/js/ddau.md +0 -20
  76. data/docs/js/extra-params.md +0 -41
  77. data/docs/js/index.md +0 -112
  78. data/docs/js/installation.md +0 -120
  79. data/docs/js/middleware.md +0 -72
  80. data/docs/js/models.md +0 -202
  81. data/docs/js/reads.md +0 -494
  82. data/docs/js/state-syncing.md +0 -100
  83. data/docs/js/writes.md +0 -373
  84. data/docs/reference/vandal.md +0 -63
  85. data/docs/reference/why.md +0 -13
  86. data/docs/topics/authorization.md +0 -155
  87. data/docs/topics/caching.md +0 -55
  88. data/docs/topics/customizing-sideloads.md +0 -156
  89. data/docs/topics/debugging.md +0 -242
  90. data/docs/topics/error-handling.md +0 -210
  91. data/docs/topics/etags.md +0 -46
  92. data/docs/topics/hopping-relationships.md +0 -149
  93. data/docs/topics/json-attributes.md +0 -77
  94. data/docs/topics/openstruct-models.md +0 -50
  95. data/docs/topics/remote-resources.md +0 -291
  96. data/docs/topics/testing.md +0 -916
  97. data/docs/topics/without-activerecord.md +0 -324
  98. data/docs/tutorial/index.md +0 -58
  99. data/docs/tutorial/step_0.md +0 -107
  100. data/docs/tutorial/step_1.md +0 -199
  101. data/docs/tutorial/step_2.md +0 -312
  102. data/docs/tutorial/step_3.md +0 -142
  103. data/docs/tutorial/step_4.md +0 -135
  104. data/docs/tutorial/step_5.md +0 -69
  105. data/docs/tutorial/step_6.md +0 -82
  106. data/docs/tutorial/step_7.md +0 -205
  107. data/docs/tutorial/step_8.md +0 -128
  108. data/docs/tutorial/step_9.md +0 -171
  109. data/docs/upgrading.md +0 -267
  110. data/website/.gitignore +0 -20
  111. data/website/README.md +0 -43
  112. data/website/docusaurus.config.js +0 -197
  113. data/website/package-lock.json +0 -19474
  114. data/website/package.json +0 -46
  115. data/website/sidebars.js +0 -82
  116. data/website/src/css/custom.css +0 -85
  117. data/website/src/pages/markdown-page.mdx +0 -7
  118. data/website/static/.nojekyll +0 -0
  119. data/website/static/1.13/2019/03/31/graphiti-1-0.html +0 -205
  120. data/website/static/1.13/2019/05/08/graphiti-1-1.html +0 -212
  121. data/website/static/1.13/2019/05/20/graphiti-1-2.html +0 -214
  122. data/website/static/1.13/2019/10/14/tutorial.html +0 -198
  123. data/website/static/1.13/CNAME +0 -1
  124. data/website/static/1.13/README.md +0 -16
  125. data/website/static/1.13/assets/css/syntax.css +0 -60
  126. data/website/static/1.13/assets/favicons/android-chrome-192x192.png +0 -0
  127. data/website/static/1.13/assets/favicons/android-chrome-256x256.png +0 -0
  128. data/website/static/1.13/assets/favicons/apple-touch-icon.png +0 -0
  129. data/website/static/1.13/assets/favicons/browserconfig.xml +0 -9
  130. data/website/static/1.13/assets/favicons/favicon-16x16.png +0 -0
  131. data/website/static/1.13/assets/favicons/favicon-32x32.png +0 -0
  132. data/website/static/1.13/assets/favicons/favicon.ico +0 -0
  133. data/website/static/1.13/assets/favicons/mstile-150x150.png +0 -0
  134. data/website/static/1.13/assets/favicons/safari-pinned-tab.svg +0 -1
  135. data/website/static/1.13/assets/favicons/site.webmanifest +0 -19
  136. data/website/static/1.13/assets/img/backend.gif +0 -0
  137. data/website/static/1.13/assets/img/conformity.png +0 -0
  138. data/website/static/1.13/assets/img/error_payload.png +0 -0
  139. data/website/static/1.13/assets/img/fancy-cushion.png +0 -0
  140. data/website/static/1.13/assets/img/gh.png +0 -0
  141. data/website/static/1.13/assets/img/home-bg.jpg +0 -0
  142. data/website/static/1.13/assets/img/lifecycle.gif +0 -0
  143. data/website/static/1.13/assets/img/logo-500.png +0 -0
  144. data/website/static/1.13/assets/img/logo.png +0 -0
  145. data/website/static/1.13/assets/img/love-graffiti.jpg +0 -0
  146. data/website/static/1.13/assets/img/meta_total_count.png +0 -0
  147. data/website/static/1.13/assets/img/persist.jpg +0 -0
  148. data/website/static/1.13/assets/img/resource.gif +0 -0
  149. data/website/static/1.13/assets/img/rest-graffiti.jpg +0 -0
  150. data/website/static/1.13/assets/img/rest1.gif +0 -0
  151. data/website/static/1.13/assets/img/rest2.gif +0 -0
  152. data/website/static/1.13/assets/img/rest3.gif +0 -0
  153. data/website/static/1.13/assets/img/rethink-rest-graffiti.jpg +0 -0
  154. data/website/static/1.13/assets/img/sunrise.png +0 -0
  155. data/website/static/1.13/assets/img/why.png +0 -0
  156. data/website/static/1.13/assets/js/highlight.pack.js +0 -2
  157. data/website/static/1.13/assets/main.css +0 -15518
  158. data/website/static/1.13/assets/main.css.map +0 -1
  159. data/website/static/1.13/bin/bundle +0 -109
  160. data/website/static/1.13/bin/jekyll +0 -27
  161. data/website/static/1.13/bin/kramdown +0 -27
  162. data/website/static/1.13/bin/listen +0 -27
  163. data/website/static/1.13/bin/rake +0 -27
  164. data/website/static/1.13/bin/rougify +0 -27
  165. data/website/static/1.13/bin/safe_yaml +0 -27
  166. data/website/static/1.13/bin/sass +0 -27
  167. data/website/static/1.13/bin/sass-convert +0 -27
  168. data/website/static/1.13/bin/scss +0 -27
  169. data/website/static/1.13/blog.html +0 -259
  170. data/website/static/1.13/cheatsheet.html +0 -316
  171. data/website/static/1.13/cookbooks/authorization.md +0 -0
  172. data/website/static/1.13/cookbooks/caching.md +0 -0
  173. data/website/static/1.13/cookbooks/customizing-sideloads.html +0 -325
  174. data/website/static/1.13/cookbooks/etags.md +0 -0
  175. data/website/static/1.13/cookbooks/hopping-relationships.html +0 -324
  176. data/website/static/1.13/cookbooks/json_attributes.md +0 -0
  177. data/website/static/1.13/cookbooks/openstruct-models.md +0 -0
  178. data/website/static/1.13/cookbooks/remote-resources.md +0 -0
  179. data/website/static/1.13/cookbooks/without-activerecord.html +0 -510
  180. data/website/static/1.13/features.html +0 -249
  181. data/website/static/1.13/feed.xml +0 -106
  182. data/website/static/1.13/guides/concepts/backends-and-models.html +0 -467
  183. data/website/static/1.13/guides/concepts/debugging.html +0 -440
  184. data/website/static/1.13/guides/concepts/endpoints.html +0 -432
  185. data/website/static/1.13/guides/concepts/error-handling.html +0 -396
  186. data/website/static/1.13/guides/concepts/links.html +0 -501
  187. data/website/static/1.13/guides/concepts/remote-resources.html +0 -536
  188. data/website/static/1.13/guides/concepts/resources.html +0 -2176
  189. data/website/static/1.13/guides/concepts/testing.html +0 -1469
  190. data/website/static/1.13/guides/getting-started/installation.html +0 -420
  191. data/website/static/1.13/guides/graphiti-rails-migration.html +0 -242
  192. data/website/static/1.13/guides/index.html +0 -269
  193. data/website/static/1.13/guides/overview.html +0 -325
  194. data/website/static/1.13/guides/upgrading-2-0.html +0 -193
  195. data/website/static/1.13/guides/upgrading.html +0 -314
  196. data/website/static/1.13/guides/vandal.html +0 -282
  197. data/website/static/1.13/guides/why.html +0 -1121
  198. data/website/static/1.13/index.html +0 -72
  199. data/website/static/1.13/js/authentication.html +0 -295
  200. data/website/static/1.13/js/ddau.html +0 -238
  201. data/website/static/1.13/js/extra-params.html +0 -270
  202. data/website/static/1.13/js/index.html +0 -321
  203. data/website/static/1.13/js/installation.html +0 -637
  204. data/website/static/1.13/js/introduction.html +0 -257
  205. data/website/static/1.13/js/middleware.html +0 -318
  206. data/website/static/1.13/js/reads/fieldsets.html +0 -271
  207. data/website/static/1.13/js/reads/filtering.html +0 -289
  208. data/website/static/1.13/js/reads/includes.html +0 -260
  209. data/website/static/1.13/js/reads/index.html +0 -497
  210. data/website/static/1.13/js/reads/nested-queries.html +0 -353
  211. data/website/static/1.13/js/reads/pagination.html +0 -260
  212. data/website/static/1.13/js/reads/sorting.html +0 -265
  213. data/website/static/1.13/js/reads/statistics.html +0 -289
  214. data/website/static/1.13/js/state-syncing.html +0 -340
  215. data/website/static/1.13/js/writes/deferred.html +0 -296
  216. data/website/static/1.13/js/writes/dirty-tracking.html +0 -399
  217. data/website/static/1.13/js/writes/index.html +0 -391
  218. data/website/static/1.13/js/writes/nested.html +0 -330
  219. data/website/static/1.13/js/writes/validations.html +0 -272
  220. data/website/static/1.13/quickstart.html +0 -660
  221. data/website/static/1.13/template +0 -161
  222. data/website/static/1.13/tutorial/index.html +0 -250
  223. data/website/static/1.13/tutorial/step_0.html +0 -292
  224. data/website/static/1.13/tutorial/step_1.html +0 -517
  225. data/website/static/1.13/tutorial/step_2.html +0 -481
  226. data/website/static/1.13/tutorial/step_3.html +0 -323
  227. data/website/static/1.13/tutorial/step_4.html +0 -318
  228. data/website/static/1.13/tutorial/step_5.html +0 -265
  229. data/website/static/1.13/tutorial/step_6.html +0 -276
  230. data/website/static/1.13/tutorial/step_7.html +0 -390
  231. data/website/static/1.13/tutorial/step_8.html +0 -316
  232. data/website/static/1.13/tutorial/step_9.html +0 -365
  233. data/website/static/CNAME +0 -1
  234. data/website/static/assets/img/error_payload.png +0 -0
  235. data/website/static/assets/img/legacy/legacy-0378a3bb39.png +0 -0
  236. data/website/static/assets/img/legacy/legacy-05bbd3e5fd.png +0 -0
  237. data/website/static/assets/img/legacy/legacy-07aa104495.png +0 -0
  238. data/website/static/assets/img/legacy/legacy-0c75a16b3a.gif +0 -0
  239. data/website/static/assets/img/legacy/legacy-3076df6209.png +0 -0
  240. data/website/static/assets/img/legacy/legacy-7f6889bc89.png +0 -0
  241. data/website/static/assets/img/legacy/legacy-a2cc4363c3.png +0 -0
  242. data/website/static/assets/img/legacy/legacy-f67cfa89ab.png +0 -0
  243. data/website/static/assets/img/meta_total_count.png +0 -0
  244. data/website/static/img/docusaurus-social-card.jpg +0 -0
  245. data/website/static/img/docusaurus.png +0 -0
  246. data/website/static/img/favicon.ico +0 -0
  247. data/website/static/img/logo.png +0 -0
  248. data/website/static/img/logo.svg +0 -1
  249. data/website/static/img/undraw_docusaurus_mountain.svg +0 -171
  250. data/website/static/img/undraw_docusaurus_react.svg +0 -170
  251. data/website/static/img/undraw_docusaurus_tree.svg +0 -40
@@ -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
- # When concurrency is disabled, take a synchronous path that mirrors the
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 Graphiti.config.concurrency
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 Graphiti.config.concurrency
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
- key = [@resource.class, record.class, record.id]
172
- @query.entity_map.compute_if_absent(key) { record }
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
- wrap_in_rails_executor do
219
- with_thread_locals(thread_storage) do
220
- with_fiber_locals(fiber_storage) do
221
- Graphiti.broadcast(:global_thread_pool_task_run, self.class.global_thread_pool_stats) do
222
- yield(*args)
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 if value == "null" && filter[:allow_nil]
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
@@ -61,7 +61,7 @@ module Graphiti
61
61
  end
62
62
 
63
63
  def page_param
64
- @page_param ||= (query_hash[:page] || {})
64
+ @page_param ||= query_hash[:page] || {}
65
65
  end
66
66
 
67
67
  def offset
@@ -75,7 +75,7 @@ module Graphiti
75
75
  end
76
76
 
77
77
  def sort_hash(attr)
78
- value = attr[0] == "-" ? :desc : :asc
78
+ value = (attr[0] == "-") ? :desc : :asc
79
79
  key = attr.sub("-", "").to_sym
80
80
 
81
81
  {key => value}
@@ -2,7 +2,7 @@ class Graphiti::Sideload::HasMany < Graphiti::Sideload
2
2
  def initialize(name, opts)
3
3
  @inverse_filter = opts[:inverse_filter]
4
4
 
5
- super(name, opts)
5
+ super
6
6
  end
7
7
 
8
8
  def type
@@ -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.config.concurrency
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
- Concurrent::Promises.zip(*promises)
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
@@ -280,9 +280,11 @@ module Graphiti
280
280
  end
281
281
 
282
282
  def load(parents, query, graph_parent)
283
- return build_resource_proxy(parents, query, graph_parent).to_a unless Graphiti.config.concurrency
284
-
285
- future_load(parents, query, graph_parent).value!
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 Graphiti.config.concurrency
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))
@@ -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: Dry::Types["strict.array"].of(map[: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
@@ -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
- 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)
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 = @object.public_send(sideload_ref.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
@@ -1,3 +1,3 @@
1
1
  module Graphiti
2
- VERSION = "2.0.0.beta.7"
2
+ VERSION = "2.0.0.beta.9"
3
3
  end
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)
@@ -20,7 +20,7 @@ namespace :graphiti do
20
20
  task audit: [:environment] do
21
21
  helpers.setup_rails!
22
22
  rows = Graphiti::Audit.run
23
- puts Graphiti::Audit::Report.new(rows).to_s
23
+ puts Graphiti::Audit::Report.new(rows)
24
24
 
25
25
  exit 1 if rows.any?(&:error?)
26
26
  end
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
- "@semantic-release/github",
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
  }