graphiti 2.0.0.beta.6 → 2.0.0.beta.8

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 (246) hide show
  1. checksums.yaml +4 -4
  2. data/.git-blame-ignore-revs +2 -0
  3. data/.github/workflows/ci.yml +55 -3
  4. data/.github/workflows/docs.yml +1 -1
  5. data/.github/workflows/notify-example-app.yml +18 -0
  6. data/.gitignore +2 -0
  7. data/.standard.yml +6 -0
  8. data/CHANGELOG.md +54 -0
  9. data/CONTRIBUTING.md +3 -1
  10. data/README.md +13 -206
  11. data/Rakefile +6 -3
  12. data/UPGRADING.md +1 -1
  13. data/graphiti.gemspec +8 -4
  14. data/lib/generators/graphiti/generator_mixin.rb +1 -1
  15. data/lib/generators/graphiti/install_generator.rb +1 -2
  16. data/lib/generators/graphiti/resource_generator.rb +6 -3
  17. data/lib/generators/graphiti/templates/application_resource.rb.erb +19 -0
  18. data/lib/generators/graphiti/templates/controller.rb.erb +26 -8
  19. data/lib/generators/graphiti/templates/index_request_spec.rb.erb +2 -2
  20. data/lib/generators/graphiti/templates/resource_reads_spec.rb.erb +4 -4
  21. data/lib/generators/graphiti/templates/show_request_spec.rb.erb +2 -2
  22. data/lib/graphiti/audit/report.rb +6 -6
  23. data/lib/graphiti/audit.rb +3 -4
  24. data/lib/graphiti/delegates/pagination.rb +2 -2
  25. data/lib/graphiti/deserializer.rb +3 -3
  26. data/lib/graphiti/errors.rb +6 -6
  27. data/lib/graphiti/extensions/temp_id.rb +1 -1
  28. data/lib/graphiti/query.rb +27 -8
  29. data/lib/graphiti/rails/context.rb +2 -1
  30. data/lib/graphiti/rails/controller.rb +3 -0
  31. data/lib/graphiti/rails/debugging.rb +1 -1
  32. data/lib/graphiti/rails/responders.rb +2 -1
  33. data/lib/graphiti/resource/configuration.rb +1 -0
  34. data/lib/graphiti/resource/dsl.rb +2 -2
  35. data/lib/graphiti/resource/interface.rb +3 -3
  36. data/lib/graphiti/resource/links.rb +1 -1
  37. data/lib/graphiti/resource/persistence.rb +1 -1
  38. data/lib/graphiti/resource/polymorphism.rb +1 -1
  39. data/lib/graphiti/resource.rb +2 -2
  40. data/lib/graphiti/resource_proxy.rb +0 -1
  41. data/lib/graphiti/runner.rb +8 -3
  42. data/lib/graphiti/scope.rb +13 -0
  43. data/lib/graphiti/scoping/filter.rb +5 -3
  44. data/lib/graphiti/scoping/paginate.rb +1 -1
  45. data/lib/graphiti/scoping/sort.rb +1 -1
  46. data/lib/graphiti/sideload/has_many.rb +1 -1
  47. data/lib/graphiti/spec_helpers/errors_proxy.rb +1 -1
  48. data/lib/graphiti/spec_helpers/matchers.rb +1 -1
  49. data/lib/graphiti/spec_helpers.rb +5 -0
  50. data/lib/graphiti/types.rb +5 -1
  51. data/lib/graphiti/util/cache_debug.rb +3 -3
  52. data/lib/graphiti/util/hash.rb +1 -1
  53. data/lib/graphiti/util/persistence.rb +6 -6
  54. data/lib/graphiti/util/serializer_relationships.rb +1 -1
  55. data/lib/graphiti/version.rb +1 -1
  56. data/lib/tasks/graphiti.rake +1 -1
  57. data/package.json +1 -0
  58. metadata +31 -197
  59. data/docs/concepts/backends-and-models.md +0 -122
  60. data/docs/concepts/endpoints.md +0 -183
  61. data/docs/concepts/links.md +0 -212
  62. data/docs/concepts/overview.md +0 -80
  63. data/docs/concepts/persisting.md +0 -376
  64. data/docs/concepts/relationships.md +0 -627
  65. data/docs/concepts/resources.md +0 -677
  66. data/docs/getting-started/first-api.md +0 -289
  67. data/docs/getting-started/installation.md +0 -185
  68. data/docs/intro.md +0 -307
  69. data/docs/js/authentication.md +0 -63
  70. data/docs/js/ddau.md +0 -20
  71. data/docs/js/extra-params.md +0 -41
  72. data/docs/js/index.md +0 -112
  73. data/docs/js/installation.md +0 -120
  74. data/docs/js/middleware.md +0 -72
  75. data/docs/js/models.md +0 -202
  76. data/docs/js/reads.md +0 -494
  77. data/docs/js/state-syncing.md +0 -100
  78. data/docs/js/writes.md +0 -373
  79. data/docs/reference/vandal.md +0 -63
  80. data/docs/reference/why.md +0 -13
  81. data/docs/topics/authorization.md +0 -155
  82. data/docs/topics/caching.md +0 -55
  83. data/docs/topics/customizing-sideloads.md +0 -156
  84. data/docs/topics/debugging.md +0 -242
  85. data/docs/topics/error-handling.md +0 -210
  86. data/docs/topics/etags.md +0 -46
  87. data/docs/topics/hopping-relationships.md +0 -149
  88. data/docs/topics/json-attributes.md +0 -77
  89. data/docs/topics/openstruct-models.md +0 -50
  90. data/docs/topics/remote-resources.md +0 -291
  91. data/docs/topics/testing.md +0 -894
  92. data/docs/topics/without-activerecord.md +0 -324
  93. data/docs/tutorial/index.md +0 -58
  94. data/docs/tutorial/step_0.md +0 -107
  95. data/docs/tutorial/step_1.md +0 -199
  96. data/docs/tutorial/step_2.md +0 -312
  97. data/docs/tutorial/step_3.md +0 -142
  98. data/docs/tutorial/step_4.md +0 -135
  99. data/docs/tutorial/step_5.md +0 -69
  100. data/docs/tutorial/step_6.md +0 -82
  101. data/docs/tutorial/step_7.md +0 -205
  102. data/docs/tutorial/step_8.md +0 -128
  103. data/docs/tutorial/step_9.md +0 -171
  104. data/docs/upgrading.md +0 -266
  105. data/website/.gitignore +0 -20
  106. data/website/README.md +0 -43
  107. data/website/docusaurus.config.js +0 -197
  108. data/website/package-lock.json +0 -19474
  109. data/website/package.json +0 -46
  110. data/website/sidebars.js +0 -82
  111. data/website/src/css/custom.css +0 -85
  112. data/website/src/pages/markdown-page.mdx +0 -7
  113. data/website/static/.nojekyll +0 -0
  114. data/website/static/1.13/2019/03/31/graphiti-1-0.html +0 -205
  115. data/website/static/1.13/2019/05/08/graphiti-1-1.html +0 -212
  116. data/website/static/1.13/2019/05/20/graphiti-1-2.html +0 -214
  117. data/website/static/1.13/2019/10/14/tutorial.html +0 -198
  118. data/website/static/1.13/CNAME +0 -1
  119. data/website/static/1.13/README.md +0 -16
  120. data/website/static/1.13/assets/css/syntax.css +0 -60
  121. data/website/static/1.13/assets/favicons/android-chrome-192x192.png +0 -0
  122. data/website/static/1.13/assets/favicons/android-chrome-256x256.png +0 -0
  123. data/website/static/1.13/assets/favicons/apple-touch-icon.png +0 -0
  124. data/website/static/1.13/assets/favicons/browserconfig.xml +0 -9
  125. data/website/static/1.13/assets/favicons/favicon-16x16.png +0 -0
  126. data/website/static/1.13/assets/favicons/favicon-32x32.png +0 -0
  127. data/website/static/1.13/assets/favicons/favicon.ico +0 -0
  128. data/website/static/1.13/assets/favicons/mstile-150x150.png +0 -0
  129. data/website/static/1.13/assets/favicons/safari-pinned-tab.svg +0 -1
  130. data/website/static/1.13/assets/favicons/site.webmanifest +0 -19
  131. data/website/static/1.13/assets/img/backend.gif +0 -0
  132. data/website/static/1.13/assets/img/conformity.png +0 -0
  133. data/website/static/1.13/assets/img/error_payload.png +0 -0
  134. data/website/static/1.13/assets/img/fancy-cushion.png +0 -0
  135. data/website/static/1.13/assets/img/gh.png +0 -0
  136. data/website/static/1.13/assets/img/home-bg.jpg +0 -0
  137. data/website/static/1.13/assets/img/lifecycle.gif +0 -0
  138. data/website/static/1.13/assets/img/logo-500.png +0 -0
  139. data/website/static/1.13/assets/img/logo.png +0 -0
  140. data/website/static/1.13/assets/img/love-graffiti.jpg +0 -0
  141. data/website/static/1.13/assets/img/meta_total_count.png +0 -0
  142. data/website/static/1.13/assets/img/persist.jpg +0 -0
  143. data/website/static/1.13/assets/img/resource.gif +0 -0
  144. data/website/static/1.13/assets/img/rest-graffiti.jpg +0 -0
  145. data/website/static/1.13/assets/img/rest1.gif +0 -0
  146. data/website/static/1.13/assets/img/rest2.gif +0 -0
  147. data/website/static/1.13/assets/img/rest3.gif +0 -0
  148. data/website/static/1.13/assets/img/rethink-rest-graffiti.jpg +0 -0
  149. data/website/static/1.13/assets/img/sunrise.png +0 -0
  150. data/website/static/1.13/assets/img/why.png +0 -0
  151. data/website/static/1.13/assets/js/highlight.pack.js +0 -2
  152. data/website/static/1.13/assets/main.css +0 -15518
  153. data/website/static/1.13/assets/main.css.map +0 -1
  154. data/website/static/1.13/bin/bundle +0 -109
  155. data/website/static/1.13/bin/jekyll +0 -27
  156. data/website/static/1.13/bin/kramdown +0 -27
  157. data/website/static/1.13/bin/listen +0 -27
  158. data/website/static/1.13/bin/rake +0 -27
  159. data/website/static/1.13/bin/rougify +0 -27
  160. data/website/static/1.13/bin/safe_yaml +0 -27
  161. data/website/static/1.13/bin/sass +0 -27
  162. data/website/static/1.13/bin/sass-convert +0 -27
  163. data/website/static/1.13/bin/scss +0 -27
  164. data/website/static/1.13/blog.html +0 -259
  165. data/website/static/1.13/cheatsheet.html +0 -316
  166. data/website/static/1.13/cookbooks/authorization.md +0 -0
  167. data/website/static/1.13/cookbooks/caching.md +0 -0
  168. data/website/static/1.13/cookbooks/customizing-sideloads.html +0 -325
  169. data/website/static/1.13/cookbooks/etags.md +0 -0
  170. data/website/static/1.13/cookbooks/hopping-relationships.html +0 -324
  171. data/website/static/1.13/cookbooks/json_attributes.md +0 -0
  172. data/website/static/1.13/cookbooks/openstruct-models.md +0 -0
  173. data/website/static/1.13/cookbooks/remote-resources.md +0 -0
  174. data/website/static/1.13/cookbooks/without-activerecord.html +0 -510
  175. data/website/static/1.13/features.html +0 -249
  176. data/website/static/1.13/feed.xml +0 -106
  177. data/website/static/1.13/guides/concepts/backends-and-models.html +0 -467
  178. data/website/static/1.13/guides/concepts/debugging.html +0 -440
  179. data/website/static/1.13/guides/concepts/endpoints.html +0 -432
  180. data/website/static/1.13/guides/concepts/error-handling.html +0 -396
  181. data/website/static/1.13/guides/concepts/links.html +0 -501
  182. data/website/static/1.13/guides/concepts/remote-resources.html +0 -536
  183. data/website/static/1.13/guides/concepts/resources.html +0 -2176
  184. data/website/static/1.13/guides/concepts/testing.html +0 -1469
  185. data/website/static/1.13/guides/getting-started/installation.html +0 -420
  186. data/website/static/1.13/guides/graphiti-rails-migration.html +0 -242
  187. data/website/static/1.13/guides/index.html +0 -269
  188. data/website/static/1.13/guides/overview.html +0 -325
  189. data/website/static/1.13/guides/upgrading-2-0.html +0 -193
  190. data/website/static/1.13/guides/upgrading.html +0 -314
  191. data/website/static/1.13/guides/vandal.html +0 -282
  192. data/website/static/1.13/guides/why.html +0 -1121
  193. data/website/static/1.13/index.html +0 -72
  194. data/website/static/1.13/js/authentication.html +0 -295
  195. data/website/static/1.13/js/ddau.html +0 -238
  196. data/website/static/1.13/js/extra-params.html +0 -270
  197. data/website/static/1.13/js/index.html +0 -321
  198. data/website/static/1.13/js/installation.html +0 -637
  199. data/website/static/1.13/js/introduction.html +0 -257
  200. data/website/static/1.13/js/middleware.html +0 -318
  201. data/website/static/1.13/js/reads/fieldsets.html +0 -271
  202. data/website/static/1.13/js/reads/filtering.html +0 -289
  203. data/website/static/1.13/js/reads/includes.html +0 -260
  204. data/website/static/1.13/js/reads/index.html +0 -497
  205. data/website/static/1.13/js/reads/nested-queries.html +0 -353
  206. data/website/static/1.13/js/reads/pagination.html +0 -260
  207. data/website/static/1.13/js/reads/sorting.html +0 -265
  208. data/website/static/1.13/js/reads/statistics.html +0 -289
  209. data/website/static/1.13/js/state-syncing.html +0 -340
  210. data/website/static/1.13/js/writes/deferred.html +0 -296
  211. data/website/static/1.13/js/writes/dirty-tracking.html +0 -399
  212. data/website/static/1.13/js/writes/index.html +0 -391
  213. data/website/static/1.13/js/writes/nested.html +0 -330
  214. data/website/static/1.13/js/writes/validations.html +0 -272
  215. data/website/static/1.13/quickstart.html +0 -660
  216. data/website/static/1.13/template +0 -161
  217. data/website/static/1.13/tutorial/index.html +0 -250
  218. data/website/static/1.13/tutorial/step_0.html +0 -292
  219. data/website/static/1.13/tutorial/step_1.html +0 -517
  220. data/website/static/1.13/tutorial/step_2.html +0 -481
  221. data/website/static/1.13/tutorial/step_3.html +0 -323
  222. data/website/static/1.13/tutorial/step_4.html +0 -318
  223. data/website/static/1.13/tutorial/step_5.html +0 -265
  224. data/website/static/1.13/tutorial/step_6.html +0 -276
  225. data/website/static/1.13/tutorial/step_7.html +0 -390
  226. data/website/static/1.13/tutorial/step_8.html +0 -316
  227. data/website/static/1.13/tutorial/step_9.html +0 -365
  228. data/website/static/CNAME +0 -1
  229. data/website/static/assets/img/error_payload.png +0 -0
  230. data/website/static/assets/img/legacy/legacy-0378a3bb39.png +0 -0
  231. data/website/static/assets/img/legacy/legacy-05bbd3e5fd.png +0 -0
  232. data/website/static/assets/img/legacy/legacy-07aa104495.png +0 -0
  233. data/website/static/assets/img/legacy/legacy-0c75a16b3a.gif +0 -0
  234. data/website/static/assets/img/legacy/legacy-3076df6209.png +0 -0
  235. data/website/static/assets/img/legacy/legacy-7f6889bc89.png +0 -0
  236. data/website/static/assets/img/legacy/legacy-a2cc4363c3.png +0 -0
  237. data/website/static/assets/img/legacy/legacy-f67cfa89ab.png +0 -0
  238. data/website/static/assets/img/meta_total_count.png +0 -0
  239. data/website/static/img/docusaurus-social-card.jpg +0 -0
  240. data/website/static/img/docusaurus.png +0 -0
  241. data/website/static/img/favicon.ico +0 -0
  242. data/website/static/img/logo.png +0 -0
  243. data/website/static/img/logo.svg +0 -1
  244. data/website/static/img/undraw_docusaurus_mountain.svg +0 -171
  245. data/website/static/img/undraw_docusaurus_react.svg +0 -170
  246. data/website/static/img/undraw_docusaurus_tree.svg +0 -40
@@ -1,25 +1,43 @@
1
1
  <% module_namespacing do -%>
2
2
  class <%= controller_class_name %> < ApplicationController
3
3
  <%- if actions?('index') -%>
4
+ <%- if responders? -%>
4
5
  def index
5
6
  <%= file_name.pluralize %> = <%= resource_klass %>.all(params)
6
- <%- if responders? -%>
7
7
  respond_with(<%= file_name.pluralize %>)
8
- <%- else -%>
9
- render jsonapi: <%= file_name.pluralize %>
10
- <%- end -%>
11
8
  end
9
+ <%- else -%>
10
+ <%- unless omit_comments? -%>
11
+ # With Graphiti::Rails::Responders included, these blocks collapse to
12
+ # respond_with(<%= file_name.pluralize %>). See https://graphiti.dev/getting-started/installation#responders
13
+ <%- end -%>
14
+ def index
15
+ <%= file_name.pluralize %> = <%= resource_klass %>.all(params)
16
+
17
+ respond_to do |format|
18
+ format.jsonapi { render jsonapi: <%= file_name.pluralize %> }
19
+ format.json { render json: <%= file_name.pluralize %> }
20
+ end
21
+ end
22
+ <%- end -%>
12
23
  <%- end -%>
13
24
  <%- if actions?('show') -%>
14
25
 
26
+ <%- if responders? -%>
15
27
  def show
16
28
  <%= file_name %> = <%= resource_klass %>.find(params)
17
- <%- if responders? -%>
18
29
  respond_with(<%= file_name %>)
19
- <%- else -%>
20
- render jsonapi: <%= file_name %>
21
- <%- end -%>
22
30
  end
31
+ <%- else -%>
32
+ def show
33
+ <%= file_name %> = <%= resource_klass %>.find(params)
34
+
35
+ respond_to do |format|
36
+ format.jsonapi { render jsonapi: <%= file_name %> }
37
+ format.json { render json: <%= file_name %> }
38
+ end
39
+ end
40
+ <%- end -%>
23
41
  <%- end -%>
24
42
  <%- if actions?('create') -%>
25
43
 
@@ -15,8 +15,8 @@ RSpec.describe "<%= type %>#index", type: :request do
15
15
  expect(<%= resource_class %>).to receive(:all).and_call_original
16
16
  make_request
17
17
  expect(response.status).to eq(200), response.body
18
- expect(d.map(&:jsonapi_type).uniq).to match_array(['<%= type %>'])
19
- expect(d.map(&:<%= id_or_rawid %>)).to match_array([<%= var %>1.id, <%= var %>2.id])
18
+ expect(jsonapi_data.map(&:jsonapi_type).uniq).to match_array(['<%= type %>'])
19
+ expect(jsonapi_data.map(&:<%= id_or_rawid %>)).to match_array([<%= var %>1.id, <%= var %>2.id])
20
20
  end
21
21
  end
22
22
  end
@@ -11,7 +11,7 @@ RSpec.describe <%= resource_class %>, type: :resource do
11
11
  expect(data.jsonapi_type).to eq('<%= type %>')
12
12
  <%- attributes.each do |a| -%>
13
13
  <%- if [:created_at, :updated_at].include?(a.name.to_sym) -%>
14
- expect(data.<%= a.name %>).to eq(datetime(<%= file_name %>.<%= a.name %>))
14
+ expect(data.<%= a.name %>).to eq(json_datetime(<%= file_name %>.<%= a.name %>))
15
15
  <%- else -%>
16
16
  expect(data.<%= a.name %>).to eq(<%= file_name %>.<%= a.name %>)
17
17
  <%- end -%>
@@ -31,7 +31,7 @@ RSpec.describe <%= resource_class %>, type: :resource do
31
31
 
32
32
  it 'works' do
33
33
  render
34
- expect(d.map(&:<%= id_or_rawid %>)).to eq([<%= var %>2.id])
34
+ expect(jsonapi_data.map(&:<%= id_or_rawid %>)).to eq([<%= var %>2.id])
35
35
  end
36
36
  end
37
37
  end
@@ -48,7 +48,7 @@ RSpec.describe <%= resource_class %>, type: :resource do
48
48
 
49
49
  it 'works' do
50
50
  render
51
- expect(d.map(&:<%= id_or_rawid %>)).to eq([
51
+ expect(jsonapi_data.map(&:<%= id_or_rawid %>)).to eq([
52
52
  <%= var %>1.id,
53
53
  <%= var %>2.id
54
54
  ]<%= sort_raw_ids %>)
@@ -62,7 +62,7 @@ RSpec.describe <%= resource_class %>, type: :resource do
62
62
 
63
63
  it 'works' do
64
64
  render
65
- expect(d.map(&:<%= id_or_rawid %>)).to eq([
65
+ expect(jsonapi_data.map(&:<%= id_or_rawid %>)).to eq([
66
66
  <%= var %>2.id,
67
67
  <%= var %>1.id
68
68
  ]<%= sort_raw_ids_descending %>)
@@ -14,8 +14,8 @@ RSpec.describe "<%= type %>#show", type: :request do
14
14
  expect(<%= resource_class %>).to receive(:find).and_call_original
15
15
  make_request
16
16
  expect(response.status).to eq(200)
17
- expect(d.jsonapi_type).to eq('<%= type %>')
18
- expect(d.<%= id_or_rawid %>).to eq(<%= var %>.id)
17
+ expect(jsonapi_data.jsonapi_type).to eq('<%= type %>')
18
+ expect(jsonapi_data.<%= id_or_rawid %>).to eq(<%= var %>.id)
19
19
  end
20
20
  end
21
21
  end
@@ -60,7 +60,7 @@ module Graphiti
60
60
 
61
61
  section_heading(label, color, ISSUE_HEADINGS.fetch(check, check.to_s.tr("_", " "))) +
62
62
  lines_grouped_by_resource(group.map { |row, finding| [row, finding.message] }) +
63
- (fixes.size == 1 ? [fix_line(fixes.first), ""] : [])
63
+ ((fixes.size == 1) ? [fix_line(fixes.first), ""] : [])
64
64
  end
65
65
 
66
66
  def loading_section
@@ -71,7 +71,7 @@ module Graphiti
71
71
  lines_grouped_by_resource(rows.map { |row| [row, nil] }) +
72
72
  [" #{paint("That is a query per record rendered.", :dim)}",
73
73
  fix_line("preload the association in base_scope, or drop `resource_ids`"),
74
- " #{paint("See www.graphiti.dev/concepts/relationships#customizing-relationships", :dim)}",
74
+ " #{paint("See graphiti.dev/concepts/relationships#customizing-relationships", :dim)}",
75
75
  ""]
76
76
  end
77
77
 
@@ -83,7 +83,7 @@ module Graphiti
83
83
  rows = @rows.select(&:would_start_loading?)
84
84
  return [] if rows.empty?
85
85
 
86
- verb = rows.size == 1 ? "renders" : "render"
86
+ verb = (rows.size == 1) ? "renders" : "render"
87
87
 
88
88
  section_heading("FYI", :blue, "#{count(rows.size, "belongs_to relationship")} #{verb} no resource ids") +
89
89
  lines_grouped_by_resource(rows.map { |row| [row, nil] }) +
@@ -95,7 +95,7 @@ module Graphiti
95
95
  ["",
96
96
  " #{paint("Either would load the association on every render. That potential performance cost is why ids are opt-in.", :dim)}",
97
97
  " #{paint("Preloading the association in base_scope keeps that load cheap.", :dim)}",
98
- " #{paint("See www.graphiti.dev/concepts/relationships#belongs-to-resource-ids", :dim)}",
98
+ " #{paint("See graphiti.dev/concepts/relationships#belongs-to-resource-ids", :dim)}",
99
99
  ""]
100
100
  end
101
101
 
@@ -189,7 +189,7 @@ module Graphiti
189
189
 
190
190
  rows.map do |cells|
191
191
  line = cells.each_with_index.map { |cell, index|
192
- index == cells.size - 1 ? cell.painted : cell.pad_to(widths[index])
192
+ (index == cells.size - 1) ? cell.painted : cell.pad_to(widths[index])
193
193
  }.join(" ")
194
194
  indent + line
195
195
  end
@@ -203,7 +203,7 @@ module Graphiti
203
203
  counts = [
204
204
  count(@rows.group_by(&:resource).size, "resource"),
205
205
  count(@rows.size, "relationship"),
206
- paint(count(errors, "error"), errors > 0 ? :red : nil)
206
+ paint(count(errors, "error"), (errors > 0) ? :red : nil)
207
207
  ]
208
208
  counts << paint("#{loading} loading ids without preloading", :yellow) if loading > 0
209
209
  counts << "#{no_ids} without resource ids" if no_ids > 0
@@ -1,7 +1,7 @@
1
1
  module Graphiti
2
2
  # Resources register themselves as they load, so eager load before auditing.
3
3
  class Audit
4
- Finding = Struct.new(:severity, :check, :message, :remedy, keyword_init: true) do
4
+ Finding = Struct.new(:severity, :check, :message, :remedy) do
5
5
  def error?
6
6
  severity == :error
7
7
  end
@@ -18,8 +18,7 @@ module Graphiti
18
18
  :resource_ids_blocker,
19
19
  :preloaded,
20
20
  :options,
21
- :findings,
22
- keyword_init: true
21
+ :findings
23
22
  ) do
24
23
  def would_start_loading?
25
24
  return false if resource_ids_default == :never
@@ -75,7 +74,7 @@ module Graphiti
75
74
  resource_ids_source_if_always: resource_ids_source_if_always(resource_class, sideload),
76
75
  resource_ids_default: resource_class.belongs_to_resource_ids_by_default,
77
76
  resource_ids_blocker: sideload.resource_ids_blocker,
78
- preloaded: source == :load ? association_preloaded?(resource_class, sideload) : nil,
77
+ preloaded: (source == :load) ? association_preloaded?(resource_class, sideload) : nil,
79
78
  options: declaration_options(sideload),
80
79
  findings: [
81
80
  missing_association_method(sideload, model),
@@ -32,7 +32,7 @@ module Graphiti
32
32
  private
33
33
 
34
34
  def pagination_params
35
- @pagination_params ||= @proxy.query.params.reject { |key, _| [:action, :controller, :format].include?(key) }
35
+ @pagination_params ||= @proxy.query.params.except(:action, :controller, :format)
36
36
  end
37
37
 
38
38
  def pagination_link(page)
@@ -109,7 +109,7 @@ module Graphiti
109
109
  end
110
110
 
111
111
  def page_param
112
- @page_param ||= (@proxy.query.hash[:page] || {})
112
+ @page_param ||= @proxy.query.hash[:page] || {}
113
113
  end
114
114
  end
115
115
  end
@@ -85,7 +85,7 @@ class Graphiti::Deserializer
85
85
  def meta(action: :update)
86
86
  {
87
87
  type: data[:type],
88
- temp_id: data[:'temp-id'],
88
+ temp_id: data[:"temp-id"],
89
89
  method: action,
90
90
  payload_path: ["data"]
91
91
  }
@@ -162,12 +162,12 @@ class Graphiti::Deserializer
162
162
  end
163
163
 
164
164
  def process_relationship_datum(datum)
165
- temp_id = datum[:'temp-id']
165
+ temp_id = datum[:"temp-id"]
166
166
  included_object = included.find { |i|
167
167
  next unless i[:type] == datum[:type]
168
168
 
169
169
  (i[:id] && i[:id] == datum[:id]) ||
170
- (i[:'temp-id'] && i[:'temp-id'] == temp_id)
170
+ (i[:"temp-id"] && i[:"temp-id"] == temp_id)
171
171
  }
172
172
  included_idx = included.index(included_object)
173
173
 
@@ -270,7 +270,7 @@ module Graphiti
270
270
  #{@resource_class.name}: error occurred while sideloading "#{@sideload_name}"!
271
271
 
272
272
  The error was raised while attempting to build query parameters for the associated Resource.
273
- Read more about sideload scoping here: www.graphiti.dev/concepts/relationships#customizing-scope
273
+ Read more about sideload scoping here: graphiti.dev/concepts/relationships#customizing-scope
274
274
 
275
275
  A good way to debug is to put a debugger within the 'params' block.
276
276
 
@@ -294,7 +294,7 @@ module Graphiti
294
294
 
295
295
  The error was raised while attempting to build the scope for the associated Resource.
296
296
 
297
- Read more about sideload scoping here: www.graphiti.dev/concepts/relationships#customizing-scope
297
+ Read more about sideload scoping here: graphiti.dev/concepts/relationships#customizing-scope
298
298
 
299
299
  Here's the original, underlying error:
300
300
 
@@ -315,7 +315,7 @@ module Graphiti
315
315
  #{@resource_class.name}: error occurred while sideloading "#{@sideload_name}"!
316
316
 
317
317
  The error was raised while attempting to assign relevant model instances. Read
318
- more about sideload assignment here: www.graphiti.dev/concepts/relationships#customizing-assignment
318
+ more about sideload assignment here: graphiti.dev/concepts/relationships#customizing-assignment
319
319
 
320
320
  A good way to debug is to put a debugger within the 'assign' block.
321
321
 
@@ -449,7 +449,7 @@ module Graphiti
449
449
 
450
450
  self.validate_endpoints = false
451
451
 
452
- See https://www.graphiti.dev/concepts/links for more information.
452
+ See https://graphiti.dev/concepts/links for more information.
453
453
 
454
454
  The current endpoints allowed for this resource are: #{@resource_class.endpoints.inspect}
455
455
  MSG
@@ -731,7 +731,7 @@ module Graphiti
731
731
 
732
732
  <<~MSG
733
733
 
734
- resource_ids is set on this relationship, so every render reads the association, not just requests that include it. See www.graphiti.dev/concepts/relationships#customizing-relationships.
734
+ resource_ids is set on this relationship, so every render reads the association, not just requests that include it. See graphiti.dev/concepts/relationships#customizing-relationships.
735
735
  MSG
736
736
  end
737
737
  end
@@ -909,7 +909,7 @@ module Graphiti
909
909
  def initialize(resource, filter_names, required)
910
910
  @resource = resource
911
911
  @filter_names = filter_names
912
- @required_label = required == :all ? "All" : "One"
912
+ @required_label = (required == :all) ? "All" : "One"
913
913
  end
914
914
 
915
915
  def message
@@ -14,7 +14,7 @@ module Graphiti
14
14
  def as_jsonapi(*)
15
15
  super.tap do |hash|
16
16
  if (temp_id = @object.instance_variable_get(:@_jsonapi_temp_id))
17
- hash[:'temp-id'] = temp_id
17
+ hash[:"temp-id"] = temp_id
18
18
  end
19
19
  end
20
20
  end
@@ -4,8 +4,15 @@ module Graphiti
4
4
  class Query
5
5
  attr_reader :resource, :association_name, :params, :action
6
6
 
7
- # TODO: make the trailing optionals keywords once the satellite gems are rolled in - see Runner#query for the nil-padding this forces
8
- def initialize(resource, params, association_name = nil, nested_include = nil, parents = [], action = nil)
7
+ def initialize(resource, params, *positional, association_name: nil, nested_include: nil, parents: [], action: nil)
8
+ if positional.any?
9
+ Graphiti::DEPRECATOR.warn("Passing Query.new trailing arguments positionally is deprecated. Use association_name:/nested_include:/parents:/action: keywords.")
10
+ association_name ||= positional[0]
11
+ nested_include ||= positional[1]
12
+ parents = positional[2] if positional.length > 2
13
+ action ||= positional[3]
14
+ end
15
+
9
16
  @resource = resource
10
17
  @association_name = association_name
11
18
  @params = params
@@ -21,6 +28,13 @@ module Graphiti
21
28
  !!@association_name
22
29
  end
23
30
 
31
+ # Concurrent::Map because sideload scopes resolve on pool threads
32
+ def entity_map
33
+ return root.entity_map unless root == self
34
+
35
+ @entity_map ||= Concurrent::Map.new
36
+ end
37
+
24
38
  def top_level?
25
39
  !association?
26
40
  end
@@ -110,9 +124,10 @@ module Graphiti
110
124
  relationship_name = sideload ? sideload.name : key
111
125
  hash[relationship_name] = Query.new sl_resource,
112
126
  @params,
113
- key,
114
- sub_hash,
115
- query_parents, :all
127
+ association_name: key,
128
+ nested_include: sub_hash,
129
+ parents: query_parents,
130
+ action: :all
116
131
  else
117
132
  handle_missing_sideload(key)
118
133
  end
@@ -124,6 +139,10 @@ module Graphiti
124
139
  @parents ||= []
125
140
  end
126
141
 
142
+ def root
143
+ parents.first || self
144
+ end
145
+
127
146
  def fields
128
147
  @fields ||= begin
129
148
  hash = parse_fieldset(@params[:fields] || {})
@@ -216,7 +235,7 @@ module Graphiti
216
235
 
217
236
  scrubbed.filter do |key, value|
218
237
  sideload = @resource.class.sideload(key)
219
- sideload.nil? ? true : sideload.readable?
238
+ sideload.nil? || sideload.readable?
220
239
  end
221
240
  end
222
241
  end
@@ -292,7 +311,7 @@ module Graphiti
292
311
  return false unless association?
293
312
 
294
313
  split = name.to_s.split(".")
295
- query_names = split[0..split.length - 2].map(&:to_sym)
314
+ query_names = split[0..-2].map(&:to_sym)
296
315
  my_names = parents.map(&:association_name).compact + [association_name].compact
297
316
  query_names == my_names
298
317
  end
@@ -324,7 +343,7 @@ module Graphiti
324
343
  end
325
344
 
326
345
  def sort_hash(attr)
327
- value = attr[0] == "-" ? :desc : :asc
346
+ value = (attr[0] == "-") ? :desc : :asc
328
347
  key = attr.sub("-", "").to_sym
329
348
 
330
349
  {key => value}
@@ -1,11 +1,12 @@
1
1
  module Graphiti
2
2
  module Rails
3
- # Wraps controller actions in a [Graphiti Context](https://www.graphiti.dev/concepts/resources#context) which points to the
3
+ # Wraps controller actions in a [Graphiti Context](https://graphiti.dev/concepts/resources#context) which points to the
4
4
  # controller instance by default.
5
5
  module Context
6
6
  def self.included(klass)
7
7
  klass.class_eval do
8
8
  include Graphiti::Context
9
+
9
10
  around_action :wrap_graphiti_context
10
11
  end
11
12
  end
@@ -17,6 +17,9 @@ module Graphiti
17
17
  included do
18
18
  include Graphiti::Rails::Context
19
19
  include Graphiti::Rails::Debugging
20
+ # ActionController::API strips this out, but Graphiti serves multiple
21
+ # formats (jsonapi/json/xml), so respond_to has to work everywhere.
22
+ include ActionController::MimeResponds
20
23
 
21
24
  # Broad, but the handler only renders formats listed in
22
25
  # config.graphiti.handled_exception_formats and hands everything else
@@ -1,6 +1,6 @@
1
1
  module Graphiti
2
2
  module Rails
3
- # Wraps controller actions in a [Graphiti Debugger](https://www.graphiti.dev/topics/debugging#debugger).
3
+ # Wraps controller actions in a [Graphiti Debugger](https://graphiti.dev/topics/debugging#debugger).
4
4
  module Debugging
5
5
  def self.included(klass)
6
6
  klass.around_action :debug_graphiti
@@ -6,6 +6,7 @@ module Graphiti
6
6
 
7
7
  included do
8
8
  include ActionController::MimeResponds
9
+
9
10
  respond_to(*Graphiti::Rails.respond_to_formats)
10
11
  end
11
12
 
@@ -14,7 +15,7 @@ module Graphiti
14
15
  opts = args.extract_options!
15
16
  opts[:location] = nil
16
17
  args << opts
17
- super(*args, &blk)
18
+ super
18
19
  end
19
20
  end
20
21
  end
@@ -60,6 +60,7 @@ module Graphiti
60
60
  def remote=(val)
61
61
  super
62
62
  include ::Graphiti::Resource::Remote
63
+
63
64
  self.endpoint = {
64
65
  path: val,
65
66
  full_path: val,
@@ -9,7 +9,7 @@ module Graphiti
9
9
  opts = args.extract_options!
10
10
  type_override = args[0]
11
11
 
12
- if (att = (attributes[name] || extra_attributes[name]))
12
+ if (att = attributes[name] || extra_attributes[name])
13
13
  # We're opting in to filtering, so force this
14
14
  # UNLESS the filter is guarded at the attribute level
15
15
  att[:filterable] = true if att[:filterable] == false
@@ -27,7 +27,7 @@ module Graphiti
27
27
  end
28
28
 
29
29
  required = att[:filterable] == :required || !!opts[:required]
30
- schema = !!opts[:via_attribute_dsl] ? att[:schema] : opts[:schema] != false
30
+ schema = (!!opts[:via_attribute_dsl]) ? att[:schema] : opts[:schema] != false
31
31
 
32
32
  config[:filters][name.to_sym] = {
33
33
  aliases: aliases,
@@ -17,7 +17,7 @@ module Graphiti
17
17
 
18
18
  # @api private
19
19
  def _all(params, opts, base_scope)
20
- runner = Runner.new(self, params, opts.delete(:query), :all)
20
+ runner = Runner.new(self, params, query: opts.delete(:query), action: :all)
21
21
  opts[:params] = params
22
22
  runner.proxy(base_scope, opts.merge(caching_options))
23
23
  end
@@ -36,7 +36,7 @@ module Graphiti
36
36
  params[:filter] ||= {}
37
37
  params[:filter][:id] = id if id
38
38
 
39
- runner = Runner.new(self, params, nil, :find)
39
+ runner = Runner.new(self, params, action: :find)
40
40
 
41
41
  find_options = {
42
42
  single: true,
@@ -56,7 +56,7 @@ module Graphiti
56
56
  # Wrap models fetched outside graphiti so they render like any other proxy
57
57
  def wrap(models, base_scope = nil)
58
58
  validate_wrap_models!(models)
59
- runner = Runner.new(self, {}, nil, :find)
59
+ runner = Runner.new(self, {}, action: :find)
60
60
  runner.proxy(base_scope, bypass_required_filters: true).tap do |proxy|
61
61
  proxy.data = models
62
62
  end
@@ -41,7 +41,7 @@ module Graphiti
41
41
  def infer_endpoint
42
42
  return unless name
43
43
 
44
- path = "/#{name.gsub("Resource", "").pluralize.underscore}".to_sym
44
+ path = :"/#{name.gsub("Resource", "").pluralize.underscore}"
45
45
  {
46
46
  path: path,
47
47
  full_path: full_path_for(path),
@@ -65,7 +65,7 @@ module Graphiti
65
65
  def add_callback(kind, lifecycle, method, only, &blk)
66
66
  config[:callbacks][kind] ||= {}
67
67
  config[:callbacks][kind][lifecycle] ||= []
68
- config[:callbacks][kind][lifecycle] << {callback: (method || blk), only: Array(only)}
68
+ config[:callbacks][kind][lifecycle] << {callback: method || blk, only: Array(only)}
69
69
  end
70
70
  end
71
71
 
@@ -56,7 +56,7 @@ module Graphiti
56
56
  on_type, name = split_on[1].split("--").map(&:to_sym)
57
57
  end
58
58
 
59
- sl = super(name)
59
+ sl = super
60
60
  if !polymorphic_child? && sl.nil?
61
61
  children.each do |c|
62
62
  next if on_type && c.type != on_type
@@ -137,14 +137,14 @@ module Graphiti
137
137
 
138
138
  # TODO: make foreign_key a keyword once the satellite gems are rolled in - they call these positionally
139
139
  def assign_with_relationships(meta, attributes, relationships, caller_model = nil, foreign_key = nil, model_instance: nil)
140
- persistence = Graphiti::Util::Persistence \
140
+ persistence = Graphiti::Util::Persistence
141
141
  .new(self, meta, attributes, relationships, caller_model, foreign_key,
142
142
  assigned_model: model_instance)
143
143
  persistence.assign
144
144
  end
145
145
 
146
146
  def persist_with_relationships(meta, attributes, relationships, caller_model = nil, foreign_key = nil, assigned_model: nil)
147
- persistence = Graphiti::Util::Persistence \
147
+ persistence = Graphiti::Util::Persistence
148
148
  .new(self, meta, attributes, relationships, caller_model, foreign_key,
149
149
  assigned_model: assigned_model)
150
150
  persistence.run
@@ -16,7 +16,6 @@ module Graphiti
16
16
  cache_expires_in: nil,
17
17
  cache_tag: nil
18
18
  )
19
-
20
19
  @resource = resource
21
20
  @scope = scope
22
21
  @query = query
@@ -3,8 +3,13 @@ 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
7
- def initialize(resource_class, params, query = nil, action = nil)
6
+ def initialize(resource_class, params, *positional, query: nil, action: nil)
7
+ if positional.any?
8
+ Graphiti::DEPRECATOR.warn("Passing query/action to Runner.new positionally is deprecated. Use query:/action: keywords.")
9
+ query ||= positional[0]
10
+ action ||= positional[1]
11
+ end
12
+
8
13
  @resource_class = resource_class
9
14
  @params = params
10
15
  @query = query
@@ -30,7 +35,7 @@ module Graphiti
30
35
  end
31
36
 
32
37
  def query
33
- @query ||= Query.new(jsonapi_resource, params, nil, nil, [], @action)
38
+ @query ||= Query.new(jsonapi_resource, params, action: @action)
34
39
  end
35
40
 
36
41
  def query_hash
@@ -154,12 +154,25 @@ module Graphiti
154
154
  payload[:results]
155
155
  }
156
156
  resolved.compact!
157
+ deduplicate_entities!(resolved)
157
158
  assign_serializer(resolved)
158
159
  yield resolved if block_given?
159
160
  @opts[:after_resolve]&.call(resolved)
160
161
  resolved
161
162
  end
162
163
 
164
+ # Must run before sideloads assign, so every include path populates the
165
+ # canonical instance. The resource class in the key keeps two resources
166
+ # serving the same model from sharing an instance and a serializer.
167
+ def deduplicate_entities!(resolved)
168
+ resolved.map! do |record|
169
+ next record unless record.respond_to?(:id) && !record.id.nil?
170
+
171
+ key = [@resource.class, record.class, record.id]
172
+ @query.entity_map.compute_if_absent(key) { record }
173
+ end
174
+ end
175
+
163
176
  def each_applicable_sideload
164
177
  @query.sideloads.each_pair do |name, sideload_query|
165
178
  sideload = @resource.class.sideload(name)
@@ -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