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,289 @@
1
+ ---
2
+ title: 'Build Your First API'
3
+ ---
4
+
5
+ # Build Your First API
6
+
7
+ By the end of this page you'll have a working Rails API, backed by Graphiti, that supports filtering, sorting, pagination, and nested relationships out of the box.
8
+
9
+ We'll use Rails and ActiveRecord here, on familiar ground. For how the pieces fit together, see [Lifecycle of a Request](/concepts/overview).
10
+
11
+ You'll need Ruby 3.2+ and Rails 7.1+ installed for this walkthrough. Graphiti itself only requires Ruby 3.2+ and ActiveSupport, so you can [use it without Rails](/getting-started/installation#without-rails).
12
+
13
+ ## Installation {#installation}
14
+
15
+ Let's start with a classic Rails blog. We'll use a [template](http://guides.rubyonrails.org/rails_application_templates.html) to handle some of the boilerplate. Run this command and accept all the defaults for now:
16
+
17
+ ```bash
18
+ $ rails new blog --api -m https://raw.githubusercontent.com/graphiti-api/graphiti_rails_template/master/all.rb
19
+ ```
20
+
21
+ Feel free to run `git diff` if you're interested in the
22
+ particulars. This is mostly installing gems and including modules.
23
+
24
+ > Note: if a network issue prevents you from pointing to this URL
25
+ > directly, you can download the file and and run this command as `-m
26
+ > /path/to/template`
27
+
28
+ Alternatively, you can [**add to an existing project**](/getting-started/installation#adding-to-an-existing-app).
29
+
30
+ ## Defining a Resource {#defining-a-resource}
31
+
32
+ A [**Resource**](/concepts/resources) defines how to query and persist your [**Model**](/concepts/backends-and-models). In other
33
+ words: a Model is to the database as Resource is to the API. So
34
+ first, let's define our Model:
35
+
36
+ ```bash
37
+ $ bundle exec rails generate model Post title:string upvotes:integer active:boolean
38
+ $ bundle exec rails db:migrate
39
+ ```
40
+
41
+ Now we can use the built-in [generator](/concepts/resources#generators) to define our Resource,
42
+ corresponding [**Endpoint**](/concepts/endpoints), and
43
+ [**Integration Tests**](/topics/testing).
44
+
45
+ ```bash
46
+ $ bundle exec rails g graphiti:resource Post title:string upvotes:integer active:boolean
47
+ ```
48
+
49
+ You'll see a number of files created. Now run your app!:
50
+
51
+ ```bash
52
+ $ bundle exec rails s
53
+ ```
54
+
55
+ Verify `http://localhost:3000/api/v1/posts` renders JSON correctly.
56
+ Now we need data.
57
+
58
+ ##### Seeding Data {#seeding-data}
59
+
60
+ Edit `db/seeds.rb` to create a few `Post`s:
61
+
62
+ ```ruby
63
+ Post.create!(title: 'My title', upvotes: 10, active: true)
64
+ Post.create!(title: 'Another title', upvotes: 20, active: false)
65
+ Post.create!(title: 'OMG! A title', upvotes: 30, active: true)
66
+ ```
67
+
68
+ And run the script:
69
+
70
+ ```bash
71
+ $ bundle exec rails db:seed
72
+ ```
73
+
74
+ Now load `http://localhost:3000/api/v1/posts`. You should have 3 `Post`s in
75
+ your database.
76
+
77
+
78
+
79
+ <hr />
80
+
81
+ ## Querying {#querying}
82
+
83
+ Now that we've defined our Resource and seeded some data, let's see
84
+ what query functionality we have. We've listed all `Post`s at `http://localhost:3000/api/v1/posts`. Let's see what we can do:
85
+
86
+ | What you want | URL |
87
+ | --- | --- |
88
+ | Sort by title, ascending | `/api/v1/posts?sort=title` |
89
+ | Sort by title, descending | `/api/v1/posts?sort=-title` |
90
+ | Paginate, 2 per page | `/api/v1/posts?page[size]=2` |
91
+ | Paginate, 2 per page, second page | `/api/v1/posts?page[size]=2&page[number]=2` |
92
+ | Sparse fieldset: only `title` | `/api/v1/posts?fields[posts]=title` |
93
+ | Filter, simple equality | `/api/v1/posts?filter[title]=my title` |
94
+ | Filter, case-insensitive equality | `/api/v1/posts?filter[title][eql]=My title` |
95
+ | Filter, prefix | `/api/v1/posts?filter[title][prefix]=my` |
96
+ | Filter, suffix | `/api/v1/posts?filter[title][suffix]=title` |
97
+ | Filter, contains | `/api/v1/posts?filter[title][match]=itl` |
98
+ | Filter, greater than | `/api/v1/posts?filter[upvotes][gt]=20` |
99
+ | Filter, greater than or equal to | `/api/v1/posts?filter[upvotes][gte]=20` |
100
+ | Filter, less than | `/api/v1/posts?filter[upvotes][lt]=20` |
101
+ | Filter, less than or equal to | `/api/v1/posts?filter[upvotes][lte]=20` |
102
+
103
+ Filtering on an attribute you haven't made filterable raises `Graphiti::Errors::InvalidAttributeAccess`. Filtering on one that doesn't exist raises `Graphiti::Errors::UnknownAttribute`. All filter logic can be customized, and customizations can be packaged into an **Adapter** for reuse. See [Filter](/concepts/resources#filter).
104
+
105
+ ### Extra Fields
106
+
107
+ Some fields are expensive enough that you only want to compute them when a client asks. Declare those with `extra_attribute`:
108
+
109
+ ```ruby
110
+ # app/resources/post_resource.rb
111
+ extra_attribute :description, :string do
112
+ @object.active? ? 'Active Post' : 'Inactive Post'
113
+ end
114
+ ```
115
+
116
+ Request it with `/api/v1/posts?extra_fields[posts]=description`. You can also eager load data conditionally when the field is requested.
117
+
118
+ ### Statistics
119
+
120
+ Useful for search grids ("the first 10 active posts, plus the total count of all posts") in a single request. Hit `/api/v1/posts?stats[total]=count` and the result arrives in the `meta` section of the response:
121
+
122
+ ![meta_total_count](/assets/img/meta_total_count.png)
123
+
124
+ Statistics respect your filters, so the count adjusts accordingly. There are several built-in stats and you can [add your own](/concepts/resources#statistics).
125
+
126
+ ### Error Handling
127
+
128
+ Your app always renders a JSONAPI-compliant error response. Raise something in the controller:
129
+
130
+ ```ruby
131
+ # app/controllers/posts_controller.rb
132
+ def index
133
+ raise 'foo'
134
+ end
135
+ ```
136
+
137
+ and this is what you'd see in production:
138
+
139
+ ![error_payload](/assets/img/error_payload.png)
140
+
141
+ Different errors can be given different response codes, JSON, and side effects. See [Error Handling](/topics/error-handling).
142
+
143
+ ## Persisting {#persisting}
144
+
145
+ Resources can Create, Update, and Delete (and you can persist multiple
146
+ Resources in a single request). The best way to observe this behavior is
147
+ to take a look at the tests the generator created. One example:
148
+
149
+ ```ruby
150
+ # spec/api/v1/employees/create_spec.rb
151
+ subject(:make_request) do
152
+ jsonapi_post "/api/v1/employees", payload
153
+ end
154
+
155
+ describe 'basic create' do
156
+ let(:payload) do
157
+ {
158
+ data: {
159
+ type: 'employees',
160
+ attributes: {
161
+ first_name: 'Jane'
162
+ }
163
+ }
164
+ }
165
+ end
166
+
167
+ it 'works' do
168
+ expect(EmployeeResource).to receive(:build).and_call_original
169
+ expect {
170
+ make_request
171
+ }.to change { Employee.count }.by(1)
172
+ expect(response.status).to eq(201)
173
+ end
174
+ end
175
+ ```
176
+
177
+ Read more about [Persistence](/concepts/persisting) and
178
+ [Testing Persistence](/topics/testing#writes).
179
+
180
+ ## Adding Relationships {#adding-relationships}
181
+
182
+ Let’s start by defining our Model:
183
+
184
+ ```bash
185
+ $ bundle exec rails g model Comment post_id:integer body:text active:boolean
186
+ $ bundle exec rails db:migrate
187
+ ```
188
+
189
+ ```ruby
190
+ # app/models/post.rb
191
+ has_many :comments
192
+
193
+ # app/models/comment.rb
194
+ belongs_to :post
195
+ ```
196
+
197
+ ...and corresponding Resource object:
198
+
199
+ ```bash
200
+ $ bundle exec rails g graphiti:resource Comment body:string active:boolean created_at:datetime
201
+ ```
202
+
203
+ Configure the relationship in `PostResource`:
204
+
205
+ ```ruby
206
+ # app/resources/post_resource.rb
207
+ has_many :comments
208
+ ```
209
+
210
+ And allow filtering Comments based on the Post `id`:
211
+
212
+ ```ruby
213
+ # app/resources/comment_resource.rb
214
+ attribute :post_id, :integer, only: [:filterable]
215
+ ```
216
+
217
+ This code:
218
+
219
+ * Allows eager-loading the relationship.
220
+ * URL: `/api/v1/posts?include=comments`
221
+ * SQL: `SELECT * FROM comments WHERE post_id = 123`
222
+ * Generates a [**Link**](/concepts/links) for
223
+ lazy-loading.
224
+ * Will use `CommentResource` for querying logic (so we can [Deep
225
+ Query](/concepts/relationships#deep-queries), e.g.
226
+ "only return the latest 3 active comments").
227
+ * By default, this will generate the query `CommentResource.all(filter: { post_id: 123 })`, but [relationships can be customized](/concepts/relationships)
228
+
229
+ You should now be able to hit `/api/v1/comments` with all the same
230
+ functionality as before. We need to seed data.
231
+
232
+ #### Seeding Relationships {#seeding-relationships}
233
+
234
+ Start by clearing out your database:
235
+
236
+ ```bash
237
+ $ bundle exec rails db:migrate:reset
238
+ ```
239
+
240
+ Replace your `db/seeds.rb` with this code to persist one `Post` and three `Comment`s:
241
+
242
+ ```ruby
243
+ comment1 = Comment.new(body: 'comment one', active: true)
244
+ comment2 = Comment.new(body: 'comment two', active: false)
245
+ comment3 = Comment.new(body: 'comment three', active: true)
246
+
247
+ Post.create! \
248
+ title: 'My title!',
249
+ active: true,
250
+ comments: [comment1, comment2, comment3]
251
+ ```
252
+
253
+ And run it:
254
+
255
+ ```bash
256
+ $ bundle exec rails db:seed
257
+ ```
258
+
259
+ ## Relationship Usage {#relationship-usage}
260
+
261
+ Now let's fetch a `Post` and filtered `Comment`s in a single request:
262
+
263
+ `/api/v1/posts?include=comments`
264
+
265
+ Any logic in `CommentResource` is available to us. Let's sort the comments by `created_at` descending:
266
+
267
+ `/api/v1/posts?include=comments&sort=-comments.created_at`.
268
+
269
+ Logic from `CommentResource` is accessible at the `/api/v1/comments` endpoint, and reusable when eager-loading Comments at `/api/v1/posts:`
270
+
271
+ * `/api/v1/comments?filter[active]=true`
272
+ * `/api/v1/posts?include=comments&filter[comments.active]=true`
273
+
274
+ This is why Resource objects exist: they provide an interface to
275
+ reuse code across multiple Endpoints.
276
+
277
+ Just as we can query a graph of Resources in a single
278
+ request, we can *persist* a graph of Resources in a single request. See
279
+ [Sideposting](/concepts/persisting#sideposting).
280
+
281
+ ## Exploring with Vandal {#exploring-with-vandal}
282
+
283
+ Graphiti ships with Vandal, a UI that introspects your schema for point-and-click data exploration. See the [Vandal Guide](/reference/vandal) to try it against this blog.
284
+
285
+ ## Next Steps {#whats-next}
286
+
287
+ * Continue with the [Tutorial](/tutorial) for a deeper walkthrough of customization and relationships.
288
+ * Browse the [Resources guide](/) for the full capability reference.
289
+ * Read the [Testing Guide](/topics/testing) to start testing your API.
@@ -0,0 +1,185 @@
1
+ ---
2
+ title: 'Installation'
3
+ ---
4
+
5
+ ## From Scratch {#from-scratch}
6
+
7
+ The easiest way to start from scratch is to use the application
8
+ template:
9
+
10
+ ```bash
11
+ $ rails new blog --api -m https://raw.githubusercontent.com/graphiti-api/graphiti_rails_template/master/all.rb
12
+ ```
13
+
14
+ Alternatively, download and point to the template locally:
15
+
16
+ ```bash
17
+ $ curl -O https://raw.githubusercontent.com/graphiti-api/graphiti_rails_template/master/all.rb
18
+ $ rails new blog --api -m all.rb
19
+ ```
20
+
21
+ Run `git diff` to see the changes to a blank Rails app.
22
+
23
+ ## Adding to an Existing App {#adding-to-an-existing-app}
24
+
25
+ This process is straightforward. You can add Graphiti to an existing
26
+ Rails app alongside [JBuilder](https://github.com/rails/jbuilder) or [ActiveModelSerializers](https://github.com/rails-api/active_model_serializers).
27
+
28
+ Start with gems:
29
+
30
+ ```ruby
31
+ # The only strictly-required gem
32
+ gem 'graphiti'
33
+
34
+ # For automatic ActiveRecord pagination
35
+ gem 'kaminari'
36
+
37
+ # Test-specific gems
38
+ group :development, :test do
39
+ gem 'rspec-rails'
40
+ gem 'factory_bot_rails'
41
+ gem 'faker'
42
+ end
43
+
44
+ group :test do
45
+ gem 'database_cleaner'
46
+ end
47
+ ```
48
+
49
+ You'll be up-and-running at this point. Verify with a simple standalone
50
+ Resource:
51
+
52
+ ```ruby
53
+ # Assuming you already have a Post ActiveRecord Model
54
+ class PostResource < Graphiti::Resource
55
+ self.adapter = Graphiti::Adapters::ActiveRecord
56
+ attribute :title, :string
57
+ end
58
+
59
+ PostResource.all.data # => [#<Post>, #<Post>, ...]
60
+ ```
61
+
62
+ Now we need to integrate with Rails endpoints (to give us things
63
+ like [#context](/concepts/resources#context)):
64
+
65
+ ```ruby
66
+ # app/controllers/application_controller.rb
67
+ class ApplicationController < ActionController::Base
68
+ include Graphiti::Rails::Controller
69
+ end
70
+ ```
71
+
72
+ And wire-up our error-handling:
73
+
74
+ ```ruby
75
+ # app/controllers/application_controller.rb
76
+ # When #show action does not find record, return 404
77
+ register_exception Graphiti::Errors::RecordNotFound,
78
+ status: 404
79
+
80
+ rescue_from Exception do |e|
81
+ handle_exception(e)
82
+ end
83
+ ```
84
+
85
+ That's it for the basics. You may have issues with generators
86
+ conflicting with your existing application structure - but you can
87
+ always write files manually or [submit an issue](https://github.com/graphiti-api/graphiti/issues).
88
+
89
+ ### Responders {#responders}
90
+
91
+ Graphiti supports JSONAPI, simple JSON, and XML. You can do this
92
+ manually when inheriting from `ActionController::Base`
93
+
94
+ ```ruby
95
+ def index
96
+ posts = PostResource.all(params)
97
+
98
+ respond_to do |format|
99
+ format.json { render(json: posts) }
100
+ format.jsonapi { render(jsonapi: posts) }
101
+ format.xml { render(xml: posts) }
102
+ end
103
+ end
104
+ ```
105
+
106
+ But we can inherit from `ActionController::API` while avoiding this
107
+ boilerplate with with the [Responders](https://github.com/plataformatec/responders) gem:
108
+
109
+ ```ruby
110
+ def index
111
+ posts = PostResource.all(params)
112
+ respond_with(posts)
113
+ end
114
+ ```
115
+
116
+ To get this functionality:
117
+
118
+ ```ruby
119
+ # Gemfile
120
+ gem 'responders'
121
+
122
+ # app/controllers/application_controller.rb
123
+ include Graphiti::Rails::Responders
124
+ ```
125
+
126
+ > Note: Persistence operations only support JSONAPI format, so you'll
127
+ > still use `render jsonapi:` and `render jsonapi_errors:` for those.
128
+
129
+ ### .graphiticfg.yml {#graphiticfg}
130
+
131
+ The `.graphiticfg.yml` file lives in the root directory of your
132
+ application. It holds configuration we need to reuse across a variety of
133
+ contexts (primarily generates and rake tasks). If you use our template to create your application, it's created for you.
134
+
135
+ Primarily this is used to hold your "API namespace":
136
+
137
+ ```yaml
138
+ namespace: /my_api/v1
139
+ ```
140
+
141
+ If this file doesn't exist you may get unexpected errors - make sure to
142
+ create it!
143
+
144
+ ### Testing {#testing}
145
+
146
+ To add our [Integration Tests](/topics/testing):
147
+
148
+ ```ruby
149
+ # Gemfile
150
+ group :development, :test do
151
+ gem 'factory_bot_rails'
152
+ gem 'rspec_rails'
153
+ gem 'faker'
154
+ end
155
+
156
+ group :test do
157
+ gem 'database_cleaner'
158
+ end
159
+ ```
160
+
161
+ Bootstrap RSpec if you haven't already:
162
+
163
+ ```bash
164
+ $ bin/rails g rspec:install
165
+ ```
166
+
167
+ Then add the Graphiti spec helpers and database cleaning to your `RSpec.configure` block. See [RSpec Setup](/topics/testing#rspec) in the Testing guide for the config to paste in.
168
+
169
+ ### will_paginate {#will-paginate}
170
+
171
+ By default, we use [Kaminari](https://github.com/kaminari/kaminari) for
172
+ ActiveRecord pagination. If you prefer [will_paginate] (or anything
173
+ else):
174
+
175
+ ```ruby
176
+ # app/resources/application_resource.rb
177
+ paginate do |scope, current_page, per_page|
178
+ scope.paginate(page: current_page, per_page: per_page)
179
+ end
180
+ ```
181
+
182
+ ## Without Rails {#without-rails}
183
+
184
+ You can use Graphiti in any plain `.rb` file. To see this in action,
185
+ check out the [Plain Ruby Sample App](https://github.com/graphiti-api/plain_ruby_example).