graphiti 2.0.0.beta.2 → 2.0.0.beta.4

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 (269) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +30 -86
  3. data/.github/workflows/docs.yml +60 -0
  4. data/.github/workflows/release.yml +8 -8
  5. data/.gitignore +7 -0
  6. data/.npmrc +9 -0
  7. data/.standard.yml +4 -1
  8. data/Appraisals +33 -32
  9. data/CHANGELOG.md +41 -0
  10. data/README.md +13 -2
  11. data/UPGRADING.md +2 -68
  12. data/docs/concepts/backends-and-models.md +122 -0
  13. data/docs/concepts/endpoints.md +183 -0
  14. data/docs/concepts/links.md +212 -0
  15. data/docs/concepts/overview.md +80 -0
  16. data/docs/concepts/persisting.md +376 -0
  17. data/docs/concepts/relationships.md +527 -0
  18. data/docs/concepts/resources.md +677 -0
  19. data/docs/getting-started/first-api.md +289 -0
  20. data/docs/getting-started/installation.md +185 -0
  21. data/docs/intro.md +307 -0
  22. data/docs/js/authentication.md +63 -0
  23. data/docs/js/ddau.md +20 -0
  24. data/docs/js/extra-params.md +41 -0
  25. data/docs/js/index.md +112 -0
  26. data/docs/js/installation.md +120 -0
  27. data/docs/js/middleware.md +72 -0
  28. data/docs/js/models.md +202 -0
  29. data/docs/js/reads.md +494 -0
  30. data/docs/js/state-syncing.md +100 -0
  31. data/docs/js/writes.md +373 -0
  32. data/docs/reference/vandal.md +63 -0
  33. data/docs/reference/why.md +13 -0
  34. data/docs/topics/authorization.md +155 -0
  35. data/docs/topics/caching.md +55 -0
  36. data/docs/topics/customizing-sideloads.md +156 -0
  37. data/docs/topics/debugging.md +216 -0
  38. data/docs/topics/error-handling.md +210 -0
  39. data/docs/topics/etags.md +46 -0
  40. data/docs/topics/hopping-relationships.md +149 -0
  41. data/docs/topics/json-attributes.md +77 -0
  42. data/docs/topics/openstruct-models.md +50 -0
  43. data/docs/topics/remote-resources.md +291 -0
  44. data/docs/topics/testing.md +894 -0
  45. data/docs/topics/without-activerecord.md +324 -0
  46. data/docs/tutorial/index.md +58 -0
  47. data/docs/tutorial/step_0.md +107 -0
  48. data/docs/tutorial/step_1.md +199 -0
  49. data/docs/tutorial/step_2.md +312 -0
  50. data/docs/tutorial/step_3.md +142 -0
  51. data/docs/tutorial/step_4.md +135 -0
  52. data/docs/tutorial/step_5.md +69 -0
  53. data/docs/tutorial/step_6.md +82 -0
  54. data/docs/tutorial/step_7.md +205 -0
  55. data/docs/tutorial/step_8.md +128 -0
  56. data/docs/tutorial/step_9.md +171 -0
  57. data/docs/upgrading.md +265 -0
  58. data/gemfiles/rails_7_1.gemfile +4 -3
  59. data/gemfiles/{rails_7_2_graphiti_rails.gemfile → rails_7_2.gemfile} +3 -3
  60. data/gemfiles/{rails_8_1_graphiti_rails.gemfile → rails_8_0.gemfile} +3 -3
  61. data/gemfiles/{rails_8_0_graphiti_rails.gemfile → rails_8_1.gemfile} +3 -3
  62. data/graphiti.gemspec +7 -5
  63. data/{deprecated_generators → lib/generators}/graphiti/api_test_generator.rb +7 -1
  64. data/{deprecated_generators → lib/generators}/graphiti/generator_mixin.rb +14 -1
  65. data/{deprecated_generators → lib/generators}/graphiti/install_generator.rb +19 -13
  66. data/{deprecated_generators → lib/generators}/graphiti/resource_generator.rb +43 -6
  67. data/{deprecated_generators → lib/generators}/graphiti/templates/index_request_spec.rb.erb +1 -1
  68. data/{deprecated_generators → lib/generators}/graphiti/templates/resource_reads_spec.rb.erb +6 -6
  69. data/{deprecated_generators → lib/generators}/graphiti/templates/show_request_spec.rb.erb +1 -1
  70. data/lib/graphiti/configuration.rb +2 -2
  71. data/lib/graphiti/error_serializers/conflict_request.rb +19 -0
  72. data/lib/graphiti/error_serializers/deprecated_constants.rb +48 -0
  73. data/lib/graphiti/error_serializers/invalid_request.rb +56 -0
  74. data/lib/graphiti/error_serializers/validation.rb +143 -0
  75. data/lib/graphiti/errors.rb +4 -23
  76. data/lib/graphiti/query.rb +1 -1
  77. data/lib/graphiti/rails/context.rb +33 -0
  78. data/lib/graphiti/rails/controller.rb +41 -0
  79. data/lib/graphiti/rails/debugging.rb +18 -0
  80. data/lib/graphiti/rails/exception_handlers.rb +77 -0
  81. data/lib/graphiti/rails/railtie.rb +139 -0
  82. data/lib/graphiti/rails/responders.rb +21 -0
  83. data/lib/graphiti/rails/test_helpers.rb +22 -0
  84. data/lib/graphiti/rails.rb +47 -29
  85. data/lib/graphiti/resource/configuration.rb +1 -0
  86. data/lib/graphiti/resource/interface.rb +2 -2
  87. data/lib/graphiti/resource/persistence.rb +14 -2
  88. data/lib/graphiti/resource/remote.rb +2 -2
  89. data/lib/graphiti/resource/sideloading.rb +1 -1
  90. data/lib/graphiti/resource.rb +13 -1
  91. data/lib/graphiti/responders.rb +7 -20
  92. data/lib/graphiti/schema.rb +5 -1
  93. data/lib/graphiti/schema_diff.rb +4 -0
  94. data/lib/graphiti/scope.rb +45 -37
  95. data/lib/graphiti/serializer.rb +6 -0
  96. data/lib/graphiti/sideload/belongs_to.rb +38 -5
  97. data/lib/graphiti/sideload/polymorphic_belongs_to.rb +27 -23
  98. data/lib/graphiti/sideload.rb +54 -35
  99. data/lib/graphiti/spec_helpers/errors.rb +73 -0
  100. data/lib/graphiti/spec_helpers/errors_proxy.rb +75 -0
  101. data/lib/graphiti/spec_helpers/helpers.rb +107 -0
  102. data/lib/graphiti/spec_helpers/node.rb +88 -0
  103. data/lib/graphiti/spec_helpers/rspec.rb +147 -0
  104. data/lib/graphiti/spec_helpers.rb +53 -0
  105. data/lib/graphiti/util/include_params.rb +2 -2
  106. data/lib/graphiti/util/persistence.rb +10 -11
  107. data/lib/graphiti/util/serializer_relationships.rb +41 -5
  108. data/lib/graphiti/version.rb +1 -1
  109. data/lib/graphiti-rails.rb +11 -0
  110. data/lib/graphiti.rb +34 -10
  111. data/lib/graphiti_errors.rb +11 -0
  112. data/lib/graphiti_spec_helpers/rspec.rb +3 -0
  113. data/lib/graphiti_spec_helpers.rb +11 -0
  114. data/lib/{graphiti/deprecated_tasks.rb → tasks/graphiti.rake} +6 -1
  115. data/package-lock.json +6199 -0
  116. data/package.json +5 -4
  117. data/website/.gitignore +20 -0
  118. data/website/README.md +43 -0
  119. data/website/docusaurus.config.js +141 -0
  120. data/website/package-lock.json +19474 -0
  121. data/website/package.json +46 -0
  122. data/website/sidebars.js +82 -0
  123. data/website/src/css/custom.css +58 -0
  124. data/website/src/pages/markdown-page.mdx +7 -0
  125. data/website/static/.nojekyll +0 -0
  126. data/website/static/1.13/2019/03/31/graphiti-1-0.html +205 -0
  127. data/website/static/1.13/2019/05/08/graphiti-1-1.html +212 -0
  128. data/website/static/1.13/2019/05/20/graphiti-1-2.html +214 -0
  129. data/website/static/1.13/2019/10/14/tutorial.html +198 -0
  130. data/website/static/1.13/CNAME +1 -0
  131. data/website/static/1.13/README.md +16 -0
  132. data/website/static/1.13/assets/css/syntax.css +60 -0
  133. data/website/static/1.13/assets/favicons/android-chrome-192x192.png +0 -0
  134. data/website/static/1.13/assets/favicons/android-chrome-256x256.png +0 -0
  135. data/website/static/1.13/assets/favicons/apple-touch-icon.png +0 -0
  136. data/website/static/1.13/assets/favicons/browserconfig.xml +9 -0
  137. data/website/static/1.13/assets/favicons/favicon-16x16.png +0 -0
  138. data/website/static/1.13/assets/favicons/favicon-32x32.png +0 -0
  139. data/website/static/1.13/assets/favicons/favicon.ico +0 -0
  140. data/website/static/1.13/assets/favicons/mstile-150x150.png +0 -0
  141. data/website/static/1.13/assets/favicons/safari-pinned-tab.svg +1 -0
  142. data/website/static/1.13/assets/favicons/site.webmanifest +19 -0
  143. data/website/static/1.13/assets/img/backend.gif +0 -0
  144. data/website/static/1.13/assets/img/conformity.png +0 -0
  145. data/website/static/1.13/assets/img/error_payload.png +0 -0
  146. data/website/static/1.13/assets/img/gh.png +0 -0
  147. data/website/static/1.13/assets/img/lifecycle.gif +0 -0
  148. data/website/static/1.13/assets/img/logo-500.png +0 -0
  149. data/website/static/1.13/assets/img/logo.png +0 -0
  150. data/website/static/1.13/assets/img/love-graffiti.jpg +0 -0
  151. data/website/static/1.13/assets/img/meta_total_count.png +0 -0
  152. data/website/static/1.13/assets/img/persist.jpg +0 -0
  153. data/website/static/1.13/assets/img/resource.gif +0 -0
  154. data/website/static/1.13/assets/img/rest-graffiti.jpg +0 -0
  155. data/website/static/1.13/assets/img/rest1.gif +0 -0
  156. data/website/static/1.13/assets/img/rest2.gif +0 -0
  157. data/website/static/1.13/assets/img/rest3.gif +0 -0
  158. data/website/static/1.13/assets/img/rethink-rest-graffiti.jpg +0 -0
  159. data/website/static/1.13/assets/img/why.png +0 -0
  160. data/website/static/1.13/assets/js/highlight.pack.js +2 -0
  161. data/website/static/1.13/assets/main.css +15518 -0
  162. data/website/static/1.13/assets/main.css.map +1 -0
  163. data/website/static/1.13/bin/bundle +109 -0
  164. data/website/static/1.13/bin/jekyll +27 -0
  165. data/website/static/1.13/bin/kramdown +27 -0
  166. data/website/static/1.13/bin/listen +27 -0
  167. data/website/static/1.13/bin/rake +27 -0
  168. data/website/static/1.13/bin/rougify +27 -0
  169. data/website/static/1.13/bin/safe_yaml +27 -0
  170. data/website/static/1.13/bin/sass +27 -0
  171. data/website/static/1.13/bin/sass-convert +27 -0
  172. data/website/static/1.13/bin/scss +27 -0
  173. data/website/static/1.13/blog.html +259 -0
  174. data/website/static/1.13/cheatsheet.html +316 -0
  175. data/website/static/1.13/cookbooks/authorization.md +0 -0
  176. data/website/static/1.13/cookbooks/caching.md +0 -0
  177. data/website/static/1.13/cookbooks/customizing-sideloads.html +325 -0
  178. data/website/static/1.13/cookbooks/etags.md +0 -0
  179. data/website/static/1.13/cookbooks/hopping-relationships.html +324 -0
  180. data/website/static/1.13/cookbooks/json_attributes.md +0 -0
  181. data/website/static/1.13/cookbooks/openstruct-models.md +0 -0
  182. data/website/static/1.13/cookbooks/remote-resources.md +0 -0
  183. data/website/static/1.13/cookbooks/without-activerecord.html +510 -0
  184. data/website/static/1.13/features.html +249 -0
  185. data/website/static/1.13/feed.xml +106 -0
  186. data/website/static/1.13/guides/concepts/backends-and-models.html +467 -0
  187. data/website/static/1.13/guides/concepts/debugging.html +440 -0
  188. data/website/static/1.13/guides/concepts/endpoints.html +432 -0
  189. data/website/static/1.13/guides/concepts/error-handling.html +396 -0
  190. data/website/static/1.13/guides/concepts/links.html +501 -0
  191. data/website/static/1.13/guides/concepts/remote-resources.html +536 -0
  192. data/website/static/1.13/guides/concepts/resources.html +2176 -0
  193. data/website/static/1.13/guides/concepts/testing.html +1469 -0
  194. data/website/static/1.13/guides/getting-started/installation.html +420 -0
  195. data/website/static/1.13/guides/graphiti-rails-migration.html +242 -0
  196. data/website/static/1.13/guides/index.html +269 -0
  197. data/website/static/1.13/guides/overview.html +325 -0
  198. data/website/static/1.13/guides/upgrading-2-0.html +193 -0
  199. data/website/static/1.13/guides/upgrading.html +314 -0
  200. data/website/static/1.13/guides/vandal.html +282 -0
  201. data/website/static/1.13/guides/why.html +1121 -0
  202. data/website/static/1.13/index.html +72 -0
  203. data/website/static/1.13/js/authentication.html +295 -0
  204. data/website/static/1.13/js/ddau.html +238 -0
  205. data/website/static/1.13/js/extra-params.html +270 -0
  206. data/website/static/1.13/js/index.html +321 -0
  207. data/website/static/1.13/js/installation.html +637 -0
  208. data/website/static/1.13/js/introduction.html +257 -0
  209. data/website/static/1.13/js/middleware.html +318 -0
  210. data/website/static/1.13/js/reads/fieldsets.html +271 -0
  211. data/website/static/1.13/js/reads/filtering.html +289 -0
  212. data/website/static/1.13/js/reads/includes.html +260 -0
  213. data/website/static/1.13/js/reads/index.html +497 -0
  214. data/website/static/1.13/js/reads/nested-queries.html +353 -0
  215. data/website/static/1.13/js/reads/pagination.html +260 -0
  216. data/website/static/1.13/js/reads/sorting.html +265 -0
  217. data/website/static/1.13/js/reads/statistics.html +289 -0
  218. data/website/static/1.13/js/state-syncing.html +340 -0
  219. data/website/static/1.13/js/writes/deferred.html +296 -0
  220. data/website/static/1.13/js/writes/dirty-tracking.html +399 -0
  221. data/website/static/1.13/js/writes/index.html +391 -0
  222. data/website/static/1.13/js/writes/nested.html +330 -0
  223. data/website/static/1.13/js/writes/validations.html +272 -0
  224. data/website/static/1.13/quickstart.html +660 -0
  225. data/website/static/1.13/template +161 -0
  226. data/website/static/1.13/tutorial/index.html +250 -0
  227. data/website/static/1.13/tutorial/step_0.html +292 -0
  228. data/website/static/1.13/tutorial/step_1.html +517 -0
  229. data/website/static/1.13/tutorial/step_2.html +481 -0
  230. data/website/static/1.13/tutorial/step_3.html +323 -0
  231. data/website/static/1.13/tutorial/step_4.html +318 -0
  232. data/website/static/1.13/tutorial/step_5.html +265 -0
  233. data/website/static/1.13/tutorial/step_6.html +276 -0
  234. data/website/static/1.13/tutorial/step_7.html +390 -0
  235. data/website/static/1.13/tutorial/step_8.html +316 -0
  236. data/website/static/1.13/tutorial/step_9.html +365 -0
  237. data/website/static/assets/img/error_payload.png +0 -0
  238. data/website/static/assets/img/legacy/legacy-0378a3bb39.png +0 -0
  239. data/website/static/assets/img/legacy/legacy-05bbd3e5fd.png +0 -0
  240. data/website/static/assets/img/legacy/legacy-07aa104495.png +0 -0
  241. data/website/static/assets/img/legacy/legacy-0c75a16b3a.gif +0 -0
  242. data/website/static/assets/img/legacy/legacy-3076df6209.png +0 -0
  243. data/website/static/assets/img/legacy/legacy-7f6889bc89.png +0 -0
  244. data/website/static/assets/img/legacy/legacy-a2cc4363c3.png +0 -0
  245. data/website/static/assets/img/legacy/legacy-f67cfa89ab.png +0 -0
  246. data/website/static/assets/img/meta_total_count.png +0 -0
  247. data/website/static/img/docusaurus-social-card.jpg +0 -0
  248. data/website/static/img/docusaurus.png +0 -0
  249. data/website/static/img/favicon.ico +0 -0
  250. data/website/static/img/logo.png +0 -0
  251. data/website/static/img/logo.svg +1 -0
  252. data/website/static/img/undraw_docusaurus_mountain.svg +171 -0
  253. data/website/static/img/undraw_docusaurus_react.svg +170 -0
  254. data/website/static/img/undraw_docusaurus_tree.svg +40 -0
  255. metadata +245 -46
  256. data/gemfiles/rails_6.gemfile +0 -18
  257. data/gemfiles/rails_6_graphiti_rails.gemfile +0 -19
  258. data/gemfiles/rails_7.gemfile +0 -18
  259. data/gemfiles/rails_7_1_graphiti_rails.gemfile +0 -19
  260. data/gemfiles/rails_7_graphiti_rails.gemfile +0 -19
  261. data/lib/graphiti/railtie.rb +0 -121
  262. /data/{deprecated_generators → lib/generators}/graphiti/resource_test_generator.rb +0 -0
  263. /data/{deprecated_generators → lib/generators}/graphiti/templates/application_resource.rb.erb +0 -0
  264. /data/{deprecated_generators → lib/generators}/graphiti/templates/controller.rb.erb +0 -0
  265. /data/{deprecated_generators → lib/generators}/graphiti/templates/create_request_spec.rb.erb +0 -0
  266. /data/{deprecated_generators → lib/generators}/graphiti/templates/destroy_request_spec.rb.erb +0 -0
  267. /data/{deprecated_generators → lib/generators}/graphiti/templates/resource.rb.erb +0 -0
  268. /data/{deprecated_generators → lib/generators}/graphiti/templates/resource_writes_spec.rb.erb +0 -0
  269. /data/{deprecated_generators → lib/generators}/graphiti/templates/update_request_spec.rb.erb +0 -0
@@ -0,0 +1,156 @@
1
+ ---
2
+ title: 'Customizing Sideloads'
3
+ ---
4
+
5
+ # Customizing Sideloads
6
+ > [See the code in our sample app](https://github.com/graphiti-api/employee_directory/commit/e5dbb24b7e5853a9f39aed455a5d318d303df37e)
7
+
8
+ This cookbook will help you understand sideloading. It would be great to
9
+ live in a world where everything follows default ActiveRecord table
10
+ conventions, but in my experience this is rarely the case. From legacy
11
+ code to alternate datastores, we need to think in Real World terms.
12
+
13
+ Our [Employee Directory](https://github.com/graphiti-api/employee_directory) sample application
14
+ has a clean schema - let's screw with it. Let's say `Department` has a column called `watcher_emails`, which is an array of strings. We want to sideload `Department > Watchers`. Though the *relationship* is called `watchers`, these will be `Employee` records.
15
+
16
+ Let's start by adding a spec:
17
+
18
+ ```ruby
19
+ # spec/resources/department/reads_spec.rb
20
+
21
+ describe 'sideloading' do
22
+ describe 'watchers' do
23
+ let!(:employee1) { create(:employee) }
24
+ let!(:employee2) { create(:employee) }
25
+ let!(:employee3) { create(:employee) }
26
+ let!(:department) do
27
+ create :department,
28
+ watcher_emails: [employee1.email, employee3.email]
29
+ end
30
+
31
+ before do
32
+ params[:include] = 'watchers'
33
+ end
34
+
35
+ it 'sideloads employees via watcher_emails' do
36
+ render
37
+ sl = d[0].sideload(:watchers)
38
+ expect(sl.map(&:id)).to eq([employee1.id, employee3.id])
39
+ expect(sl.map(&:jsonapi_type).uniq).to eq(['employees'])
40
+ end
41
+ end
42
+ end
43
+ ```
44
+
45
+ Add the relationship:
46
+
47
+ ```ruby
48
+ # app/resources/department_resource.rb
49
+ has_many :watchers, resource: EmployeeResource
50
+ ```
51
+
52
+ Run the test and you'll get this error:
53
+
54
+ ```error
55
+ Graphiti::Errors::AttributeError:
56
+ EmployeeResource: Tried to filter on attribute :department_id, but could not find an attribute with that name.
57
+ ```
58
+
59
+ How would we track down this error? Well, we know Resources connect
60
+ together with [Links](/concepts/links). Let's
61
+ take a look at the query parameters that would be used to connect these
62
+ two Resources:
63
+
64
+ ```ruby
65
+ has_many :watchers, resource: EmployeeResource do
66
+ params do |hash, departments|
67
+ binding.pry
68
+ end
69
+ end
70
+ ```
71
+
72
+ > Note - we're using [pry](https://github.com/pry/pry) to debug here.
73
+
74
+ The value of `hash` here is:
75
+
76
+ ```ruby
77
+ { filter: { department_id: "1" } }
78
+ ```
79
+
80
+ Which makes sense. If we say `has_many :things`, by default we expect `Thing` to have a `department_id` we can query.
81
+
82
+ That's not our case, though. Instead, let's customize those parameters
83
+ to fit our use case:
84
+
85
+ ```ruby
86
+ params do |hash, departments|
87
+ emails = departments.map(&:watcher_emails).flatten
88
+ hash[:filter] = { email: emails }
89
+ end
90
+ ```
91
+
92
+ Instead of querying by `department_id`, we need to query by `email`. And
93
+ the value we pass in will be an array of email addresses
94
+
95
+ We'd need to add an `email` filter to `EmployeeResource` to make this
96
+ work. This gets us ***querying*** correctly, but there's another error:
97
+
98
+ ```error
99
+ undefined method `department_id' for #<Employee:0x00007f9652ae6d80>
100
+ ```
101
+
102
+ Here's the thing to keep in mind: let's say our request was
103
+ `/departments?include=watchers`. We queried all the data, and we now have an array of `Department`s and an array of `Employee`s. Now we need
104
+ to specify which employees should be assigned as watchers of which
105
+ department.
106
+
107
+ Let's write that code manually:
108
+
109
+ ```ruby
110
+ has_many :watchers, resource: EmployeeResource do
111
+ # ... code ...
112
+ assign do |departments, employees|
113
+ departments.each do |d|
114
+ d.watchers = employees.select do |e|
115
+ e.email.in?(d.watcher_emails)
116
+ end
117
+ end
118
+ end
119
+ end
120
+ ```
121
+
122
+ We're selecting all relevant `Employee`s for a given `Department` by checking the array of `watcher_emails`.
123
+
124
+ This code can be tightened up a little with `assign_each` (recommended).
125
+ This way we don't have to iterate departments or worry about the
126
+ assignment ourselves:
127
+
128
+ ```ruby
129
+ has_many :watchers, resource: EmployeeResource do
130
+ # ... code ...
131
+
132
+ assign_each do |department, employees|
133
+ employees.select { |e| e.email.in?(d.watcher_emails) }
134
+ end
135
+ end
136
+ ```
137
+
138
+ We're using `#select` to return an array of relevant `Employee`s. If this was a `belongs_to` or `has_one` relationship, we'd probably want to use `#find` to return a single `Employee`.
139
+
140
+ OK there's *one last error*:
141
+
142
+ ```error
143
+ undefined method `watchers=' for #<Department:0x00007feb625a7468>
144
+ ```
145
+
146
+ This one is simple - the `assign` function will call your Adapter's assignment logic, which by default will be a simple `department.watchers
147
+ = relevant_employees`. That means we need to add a getter/setter for
148
+ this property:
149
+
150
+ ```ruby
151
+ # app/models/department.rb
152
+ attr_accessor :watchers
153
+ ```
154
+
155
+ And we're done! The test should now pass. [Check out the working code
156
+ here](https://github.com/graphiti-api/employee_directory/tree/customize_sideloads_cookbook).
@@ -0,0 +1,216 @@
1
+ ---
2
+ title: 'Debugging'
3
+ ---
4
+
5
+ ## Debugger {#debugger}
6
+
7
+ Graphiti comes with a debugger that shows the queries executed for a
8
+ given request. Remember that Resources [have a query interface](/concepts/resources#query-interface) independent of a request or response. And Resources connect similar to ActiveRecord's `includes`:
9
+
10
+ ```ruby
11
+ employees = EmployeeResource.all
12
+ PositionResource.all(filter: { employee_id: employees.map(&:id) })
13
+ ```
14
+
15
+ > *Remember, this is all [customizable](/concepts/relationships#customizing-relationships)*.
16
+
17
+ That means we can log the requests made by individual Resources:
18
+
19
+ `/api/v1/employees?include=notes,positions.department.teams`
20
+ <p align="center">
21
+ <img width="100%" src="/assets/img/legacy/legacy-a2cc4363c3.png" />
22
+ </p>
23
+
24
+ And even copy/paste these queries into a console session to debug:
25
+
26
+ ```bash
27
+ $ bin/rails c
28
+ >> TeamResource.all({:filter=>{:department_id=>"1,2,3"}})
29
+ ```
30
+
31
+ If you're having trouble with a request, see if you can isolate to a
32
+ specific Resource, then test that Resource directly.
33
+
34
+ Finally: if an error occurs, we'll note the query that caused it:
35
+
36
+ <p align="center">
37
+ <img width="100%" src="/assets/img/legacy/legacy-05bbd3e5fd.png" />
38
+ </p>
39
+
40
+ ### JSON Output {#json-output}
41
+
42
+ It can be helpful to have this debug output come back as part of the
43
+ JSON response. To enable this:
44
+
45
+ ```ruby
46
+ # app/controllers/application_controller.rb
47
+ def allow_graphiti_debug_json?
48
+ true
49
+ # or, current_user.admin?
50
+ # or, Rails.env.development?
51
+ end
52
+ ```
53
+
54
+ And request the debug output:
55
+
56
+ `/your/url?debug=true`
57
+
58
+ You should now see the debug output in `meta`:
59
+
60
+ <p align="center">
61
+ <img width="100%" src="/assets/img/legacy/legacy-7f6889bc89.png" />
62
+ </p>
63
+
64
+ <br />
65
+
66
+ If there's an error, and you've [enabled raw errors](/topics/error-handling#displaying-raw-errors), you'll also see the query that caused the error in the JSON response:
67
+
68
+ <br />
69
+
70
+ <p align="center">
71
+ <img width="100%" src="/assets/img/legacy/legacy-f67cfa89ab.png" />
72
+ </p>
73
+
74
+ <br />
75
+
76
+ ### Configuration {#configuration}
77
+
78
+ By default, we'll log to `Rails.logger`, and only enable debugging (logs or JSON) when `Rails.logger.level` is set to `debug`. Here are the
79
+ various ways to configure.
80
+
81
+ Use `config.debug` to explicitly toggle debugging:
82
+
83
+ ```ruby
84
+ # config/initializers/graphiti.rb
85
+ Graphiti.configure do |c|
86
+ c.debug = false
87
+ end
88
+
89
+ # Or use environment variable
90
+ # GRAPHITI_DEBUG=false
91
+ ```
92
+
93
+ Use `config.debug_models` to get additional (but verbose) output:
94
+
95
+ <p align="center">
96
+ <img width="100%" src="/assets/img/legacy/legacy-3076df6209.png" />
97
+ </p>
98
+
99
+ ```ruby
100
+ # config/initializers/graphiti.rb
101
+ Graphiti.configure do |c|
102
+ c.debug_models = true
103
+ end
104
+
105
+ # Or use environment variable
106
+ # GRAPHITI_DEBUG_MODELS=true
107
+ ```
108
+
109
+ As noted above, `allow_graphiti_debug_json?` must return `true` if you
110
+ want JSON output:
111
+
112
+ ```ruby
113
+ # app/controllers/application_controller.rb
114
+ def allow_graphiti_debug_json?
115
+ true
116
+ # or, current_user.admin?
117
+ # or, Rails.env.development?
118
+ end
119
+ ```
120
+
121
+ Note you need to explicitly pass `?debug=true` in the request.
122
+
123
+ Assign a different logger:
124
+
125
+ ```ruby
126
+ Graphiti.logger = Logger.new(...)
127
+
128
+ # Or the built-in STDOUT logger:
129
+ Graphiti.logger = Graphiti.stdout_logger
130
+ ```
131
+
132
+ Manually apply the debugging (when using Rails, this normally happens in
133
+ a `around_action`):
134
+
135
+ ```ruby
136
+ Graphiti::Debugger.debug do
137
+ EmployeeResource.all
138
+ end
139
+ ```
140
+
141
+ ### Rake Tasks {#rake-tasks}
142
+
143
+ There are some common debugging scenarios that are possible to do
144
+ manually, but their frequency warrants common patterns. For these, we
145
+ have rake tasks.
146
+
147
+ #### graphiti:request {#graphiti-request}
148
+
149
+ > `bin/rake graphiti:request[PATH,DEBUG]`
150
+
151
+ Execute a request using `ActionDispatch::Integration::Session` (which
152
+ underlies request specs).
153
+
154
+ This can be helpful when you don't have, or don't want to spin up, a web
155
+ server. Imagine you want to debug something on production, so you shell
156
+ into a docker container and edit some files locally. Now you want to
157
+ execute a request and see if your changes worked:
158
+
159
+ ```bash
160
+ $ bin/rake graphiti:request[/employees]
161
+ ```
162
+
163
+ Will execute the request and spit out the JSON response. You may want to
164
+ run with the Debugger enabled:
165
+
166
+ ```bash
167
+ $ bin/rake graphiti:request[/employees,true]
168
+ ```
169
+
170
+ Which add Debugger output as well.
171
+
172
+ The `PATH` should not contain the domain unless you want to hit a live
173
+ API instead of a test server.
174
+
175
+ #### graphiti:benchmark {#graphiti-benchmark}
176
+
177
+ > `bin/rake graphiti:benchmark[PATH,NUM_REQUESTS]`
178
+
179
+ It can be helpful to run a quick benchmark without hitting a live web
180
+ server, to eliminate the vagaries of latency. To do this:
181
+
182
+ ```bash
183
+ $ bin/rake graphiti:benchmark[/employees,100]
184
+ ```
185
+
186
+ Which will return the average response time.
187
+
188
+ #### Authorization headers {#Authorization-headers}
189
+
190
+ If you have an Authorization scheme implemented (for example [authenticate_or_request_with_http_token](https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Token.html) in rails) you can supply the `Authorization` http header value with the `AUTHORIZATION_HEADER` environment variable:
191
+
192
+ ```bash
193
+ $ export AUTHORIZATION_HEADER="Token --PRIVATE_API_KEY--"
194
+ $ bin/rake graphiti:request[/employees,true]
195
+ ```
196
+
197
+ This also will work for `Basic` ([request_http_basic_authentication](https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Basic.html$$)) and `Bearer` values
198
+
199
+ ## Tips {#tips}
200
+
201
+ When debugging an application, try to isolate the individual Resource
202
+ call and debug the Resource directly (instead of running the entire
203
+ request). This helps eliminate variables, and plain ruby code is easier
204
+ to work with. If possible, try to remove Graphiti entirely and focus on
205
+ your Models and Backends.
206
+
207
+ The most common scenario is debugging a query. We suggest overriding
208
+ `resolve` and using [pry](https://github.com/pry/pry) (or equivalent):
209
+
210
+ ```ruby
211
+ # Introspect the scope without firing a query
212
+ # Call 'super' to fire the query
213
+ def resolve(scope)
214
+ binding.pry
215
+ end
216
+ ```
@@ -0,0 +1,210 @@
1
+ ---
2
+ title: 'Error Handling'
3
+ ---
4
+
5
+ ## Overview {#overview}
6
+
7
+ Whenever we have an application error, we want to respond with a
8
+ [JSONAPI-compliant errors payload](http://jsonapi.org/format/#errors).
9
+ This way clients have a predictable response detailing information about
10
+ the error.
11
+
12
+ ```json
13
+ {
14
+ "errors": [
15
+ {
16
+ "code": "internal_server_error",
17
+ "status": "500",
18
+ "title": "Internal Server Error",
19
+ "detail": "We've notified our engineers and hope to address this issue shortly."
20
+ }
21
+ ]
22
+ }
23
+ ```
24
+
25
+ We'll also need a way to customize this payload. For instance, if a
26
+ `NotAuthorized` error is raised, the response should have a `403` status
27
+ code. For other errors, we may want to render a helpful error message:
28
+
29
+ ```ruby
30
+ class ApplicationController < ActionController::API
31
+ register_exception NotAuthorized, status: 403
32
+ register_exception ShipmentDelayed,
33
+ detail: ->(e) { "Contact us at 123-456-7899" }
34
+ # ... code ...
35
+ end
36
+ ```
37
+
38
+ Exception handling lives in Graphiti's Rails integration. Customizing the behavior based on error class happens in the [RescueRegistry](https://github.com/wagenet/rescue_registry) dependency.
39
+
40
+ ### Setup {#setup}
41
+
42
+ Include the Rails integration in the controllers serving your resources:
43
+
44
+ ```ruby
45
+ class ApplicationController < ActionController::Base
46
+ include Graphiti::Rails::Controller
47
+ end
48
+ ```
49
+
50
+ That registers handlers for Graphiti's own exceptions and renders anything else as JSON:API. `register_exception` itself is available on every controller without it. See below.
51
+
52
+ #### Displaying Raw Errors {#displaying-raw-errors}
53
+
54
+ When raw errors are on, the same payload carries the underlying exception under `meta.__raw_error__`:
55
+
56
+ ```json
57
+ {
58
+ "errors": [
59
+ {
60
+ "code": "internal_server_error",
61
+ "status": "500",
62
+ "title": "Internal Server Error",
63
+ "detail": "We've notified our engineers and hope to address this issue shortly.",
64
+ "meta": {
65
+ "__raw_error__": {
66
+ "message": "EmployeesController::SomeError",
67
+ "backtrace": [
68
+ "app/controllers/employees_controller.rb:5:in `index'",
69
+ "..."
70
+ ]
71
+ }
72
+ }
73
+ }
74
+ ]
75
+ }
76
+ ```
77
+
78
+
79
+ It can be useful to display the raw error as part of the JSON response -
80
+ but you probably don't want to expose your stack trace to customers.
81
+ Let's only show raw errors for the `staging` environment:
82
+
83
+ ```ruby
84
+ class ApplicationController < ActionController::API
85
+ # ... code ...
86
+
87
+ def show_detailed_exceptions?
88
+ Rails.env.staging?
89
+ end
90
+ end
91
+ ```
92
+
93
+ Another common pattern is to only show raw errors when the user is
94
+ privileged to see them:
95
+
96
+ ```ruby
97
+ class ApplicationController < ActionController::API
98
+ # ... code ...
99
+
100
+ def show_detailed_exceptions?
101
+ current_user.admin?
102
+ end
103
+ end
104
+
105
+ ```
106
+
107
+ When `#show_detailed_exceptions?` returns `true`, you'll get the raw error class,
108
+ message, and backtrace in the JSON response.
109
+
110
+ ## Usage {#usage}
111
+
112
+ ### Basic {#basic}
113
+
114
+ Let's register an error with a custom response code:
115
+
116
+ ```ruby
117
+ register_exception Errors::NotAuthorized, status: 403
118
+ ```
119
+
120
+ Now if we `raise Errors::NotAuthorized`, the response code will be `403`.
121
+
122
+ Additional options:
123
+
124
+ ```ruby
125
+ register_exception Errors::NotAuthorized,
126
+ status: 403,
127
+ title: "You cannot perform this action",
128
+ detail: :exception, # render the raw error message
129
+ detail: ->(error) { "Invalid Action" } # message via proc
130
+ ```
131
+
132
+ [See full documentation in the RescueRegistry README](https://github.com/wagenet/rescue_registry).
133
+
134
+ All controllers will inherit any registered exceptions from their parent. They can also add their own. In this example, `FooError` will only throw a custom status code when thrown from `FooController`:
135
+
136
+ ```ruby
137
+ class FooController < ApplicationController
138
+ register_exception FooError, status: 422
139
+ end
140
+ ```
141
+
142
+ ### Advanced {#advanced}
143
+
144
+ The final option `register_exception` accepts is `handler`. Here you can inject your own error handling class that customize `RescueRegistry::ExceptionHandler`. For example:
145
+
146
+ ```ruby
147
+ class MyCustomHandler < Graphiti::Rails::ExceptionHandler
148
+ # self.exception accessible within all instance methods
149
+
150
+ def status_code
151
+ # ...customize...
152
+ end
153
+
154
+ def error_code
155
+ # ...customize...
156
+ end
157
+
158
+ def title
159
+ # ...customize...
160
+ end
161
+
162
+ def detail
163
+ # ...customize...
164
+ end
165
+
166
+ def meta
167
+ # ...customize...
168
+ end
169
+ end
170
+
171
+ register_exception FooError, handler: MyCustomHandler
172
+ ```
173
+
174
+ If you would like to use the same custom handler for all errors, override `default_exception_handler`:
175
+
176
+ ```ruby
177
+ # app/controllers/application_controller.rb
178
+ def self.default_exception_handler
179
+ MyCustomHandler
180
+ end
181
+ ```
182
+
183
+ ## Testing {#testing}
184
+
185
+ This pattern of globally rescuing exceptions makes sense when
186
+ running our live application...but during testing, we may want to
187
+ raise real errors and bypass this rescue logic.
188
+
189
+ This is why we turn off error-handling during tests by default:
190
+
191
+ ```ruby
192
+ # spec/rails_helper.rb
193
+ RSpec.configure do |config|
194
+ config.include Graphiti::Rails::TestHelpers
195
+ # ... code ...
196
+
197
+ config.before :each do
198
+ handle_request_exceptions(false)
199
+ end
200
+ end
201
+ ```
202
+
203
+ If you want to turn this on for an individual test (so you can test
204
+ error codes, etc):
205
+
206
+ ```ruby
207
+ before do
208
+ handle_request_exceptions(true)
209
+ end
210
+ ```
@@ -0,0 +1,46 @@
1
+ ---
2
+ title: 'ETags'
3
+ ---
4
+
5
+ # ETags
6
+
7
+ Every resource proxy can compute a weak ETag for its current result set via `proxy.etag`. It's a plain string. Graphiti doesn't wire up `If-None-Match` handling or send `304 Not Modified` responses itself, so using it for HTTP conditional requests is up to your controller (for example, with Rails' own `fresh_when`/`stale?`).
8
+
9
+ ## How it's computed
10
+
11
+ `etag` is a weak ETag built from the same cache key used for [caching](/topics/caching), but the *versioned* one:
12
+
13
+ ```ruby
14
+ def etag
15
+ "W/#{ActiveSupport::Digest.hexdigest(cache_key_with_version.to_s)}"
16
+ end
17
+ ```
18
+
19
+ `cache_key_with_version` combines the scope's versioned cache key (which folds in every sideloaded proxy's versioned cache key and the underlying object's own `cache_key_with_version`), the query's cache key, and the resource cache tag if one is configured. Those are the same three ingredients described in the caching doc, except the scope portion here is version-aware rather than the plain identity-only key. In practice this means the ETag changes whenever the resolved records' `updated_at` values change, or whenever the rendering-relevant query params (fields, extra_fields, links, pagination_links, format) change.
20
+
21
+ Because it's derived purely from `cache_key_with_version`, calling `etag` twice on equivalent proxies (same resource, scope, and query) produces the same weak ETag, and it's always prefixed with `W/`.
22
+
23
+ ## Using it
24
+
25
+ Since there's no built-in controller integration, you compute and use it explicitly:
26
+
27
+ ```ruby
28
+ def index
29
+ employees = EmployeeResource.all(params)
30
+ response.headers["ETag"] = employees.etag
31
+ render jsonapi: employees
32
+ end
33
+ ```
34
+
35
+ Or combine it with Rails' conditional-GET support if you want automatic `304` handling:
36
+
37
+ ```ruby
38
+ def index
39
+ employees = EmployeeResource.all(params)
40
+ fresh_when(etag: employees.etag)
41
+ end
42
+ ```
43
+
44
+ ## Relationship to resource-level caching
45
+
46
+ `etag` doesn't require `cache_resource` or `Graphiti.config.cache_rendering = true`. It's available on any resource proxy regardless of whether that resource participates in rendering caching. It does, however, share its key ingredients with the cache-rendering machinery: the same `cache_key_with_version` that ETags are hashed from is also what `Graphiti::Util::CacheDebug` tracks (as `current_version[:etag]` / `last_version[:etag]`) when the [debugger](/topics/debugging) reports on cache-key changes for a cached resource. So if you're seeing an ETag change unexpectedly, the debugger's cache section (enabled the same way as for [caching](/topics/caching)) will show you which cache-key segment changed.