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,324 @@
1
+ ---
2
+ title: 'Usage Without ActiveRecord'
3
+ ---
4
+
5
+ # Usage Without ActiveRecord
6
+ Graphiti was built to be used with any ORM or datastore, from PostgreSQL
7
+ to elasticsearch to `Net::HTTP`. In fact, Graphiti itself is tested with
8
+ Plain Old Ruby Objects (POROs).
9
+
10
+ This cookbook will show how to customize a resource around a particular datastore, and how to package those
11
+ customizations into a reusable adapter. We'll use an in-memory datastore
12
+ and Plain Old Ruby Objects (POROs) here, but the lessons apply to any
13
+ datastore.
14
+
15
+ For working code, see [this branch of the sample application](https://github.com/graphiti-api/employee_directory/blob/poro/app/resources/post_resource.rb).
16
+
17
+ We'll start with this PORO model:
18
+
19
+ ```ruby
20
+ class Post
21
+ # Define getters/setters
22
+ # e.g. post.title = 'foo'
23
+ ATTRS = [:id, :title]
24
+ ATTRS.each { |a| attr_accessor(a) }
25
+
26
+ # Instantiate with hash of attributes
27
+ # e.g. Post.new(title: 'foo')
28
+ def initialize(attrs = {})
29
+ attrs.each_pair { |k,v| send(:"#{k}=", v) }
30
+ end
31
+
32
+ # This part only needed for our particular
33
+ # persistence implementation; you may not need it
34
+ # e.g. post.attributes # => { title: 'foo' }
35
+ def attributes
36
+ {}.tap do |attrs|
37
+ ATTRS.each do |name|
38
+ attrs[name] = send(name)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ ```
44
+
45
+ And this in-memory datastore:
46
+
47
+ ```ruby
48
+ # If we were working with more than just Posts, we'd need a 'type'
49
+ # field here as well, to simulate a table name.
50
+ DATA = [
51
+ { id: 1, title: 'Graphiti' },
52
+ { id: 2, title: 'is' },
53
+ { id: 3, title: 'super' },
54
+ { id: 4, title: 'dope' }
55
+ ]
56
+ ```
57
+
58
+ ## Resource Overrides {#resource-overrides}
59
+
60
+ If it's your first time with a new ORM or datastore, we recommend
61
+ putting the logic in the Resource first. Once things are working *and*
62
+ there are multiple uses of the same overrides, package them into an
63
+ Adapter.
64
+
65
+ ```ruby
66
+ class PostResource < ApplicationResource
67
+ self.adapter = Graphiti::Adapters::Null
68
+
69
+ attribute :title, :string
70
+
71
+ def base_scope
72
+ {}
73
+ end
74
+
75
+ def resolve(scope)
76
+ DATA.map { |d| Post.new(d) }
77
+ end
78
+ end
79
+ ```
80
+
81
+ Here we're using the `Null` adapter, which acts as a dumb pass-through.
82
+ This can be helpful when you just want to get running for a simple use
83
+ case and don't want errors around features you haven't implemented yet.
84
+ But it can also be confusing when you expect certain codepaths to
85
+ be hit. Mostly just be aware of `Null`'s behavior, or use `Graphiti::Adapters::Abstract` to get helpful errors around what's not
86
+ implemented.
87
+
88
+ We're also supplying an explicit `base_scope`. This is the beginning
89
+ query object we'll modify as params come in. In the case of
90
+ ActiveRecord, we might want an `ActiveRecord::Relation` like `Post.all`. For our example, we'll modify a simple ruby hash (keep in
91
+ mind the premise of building a hash of options and passing it off to a
92
+ client can apply to any datastore).
93
+
94
+ Finally, we're [resolving that scope](/concepts/resources#resolve),
95
+ returning the full dataset for now. The contract of `#resolve` is to return an array of model instances, hence `DATA.map { |d| Post.new(d)
96
+ }`.
97
+
98
+ #### Sorting {#sorting}
99
+
100
+ ```ruby
101
+ sort_all do |scope, attribute, direction|
102
+ scope[:sort].merge!(attribute: att, direction: dir)
103
+ end
104
+
105
+ def base_scope
106
+ { sort: {} }
107
+ end
108
+
109
+ def resolve(scope)
110
+ if sort = scope[:sort].presence
111
+ data = DATA.sort_by { |d| d[sort[:attribute].to_sym] }
112
+ data = data.reverse if sort[:direction] == :desc
113
+ end
114
+ DATA.map { |d| Post.new(d) }
115
+ end
116
+ ```
117
+
118
+ We modified the base scope with a default hash key, `:sort`. When the
119
+ user requests sorting, we record this by merging into the hash. We can
120
+ then reference that information on the scope when resolving.
121
+
122
+ Note the `sort_all` scope block, in fact all scope blocks, must return the scope.
123
+
124
+ #### Paginating {#paginating}
125
+
126
+ ```ruby
127
+ paginate do |scope, current_page, per_page|
128
+ scope.merge!(current_page: current, per_page: per)
129
+ end
130
+
131
+ def resolve(scope)
132
+ # ... sorting ...
133
+ start = (scope[:current_page] - 1) * scope[:per_page]
134
+ stop = start + scope[:per_page]
135
+ data = data[start...stop]
136
+ # ... return models ...
137
+ end
138
+ ```
139
+
140
+ Again: merge into the scope, then reference the scope data when
141
+ resolving.
142
+
143
+ #### Filtering {#filtering}
144
+
145
+ ```ruby
146
+ filter :title, only: [:eq] do
147
+ eq do |scope, value|
148
+ scope[:filters][attribute] = value
149
+ scope
150
+ end
151
+ end
152
+
153
+ def base_scope(*)
154
+ { sort: {}, filters: {} }
155
+ end
156
+
157
+ def resolve(scope)
158
+ # ... sorting ...
159
+ scope[:filters].each_pair do |k, v|
160
+ data = data.select { |d| d[k.to_sym].in?(v) }
161
+ end
162
+ # ... pagination ...
163
+ # ... return models ...
164
+ end
165
+ ```
166
+
167
+ Same as above examples. Again, we must return the scope object
168
+ from the filter function.
169
+
170
+ #### Persisting {#persisting}
171
+
172
+ All at once:
173
+
174
+ ```ruby
175
+ # Instantiate a model for #create
176
+ def build(model_class)
177
+ model_class.new
178
+ end
179
+
180
+ # Used for create/update
181
+ def assign_attributes(model, attributes)
182
+ attributes.each_pair do |k, v|
183
+ model.send(:"#{k}=", v)
184
+ end
185
+ end
186
+
187
+ # Used for create/update
188
+ def save(model)
189
+ attrs = model.attributes.dup
190
+ attrs[:id] ||= DATA.length + 1
191
+ if existing = DATA.find { |d| d[:id].to_s == attrs[:id].to_s }
192
+ existing.merge!(attrs)
193
+ else
194
+ DATA << attrs
195
+ end
196
+ model
197
+ end
198
+
199
+ # Used for destroy
200
+ def delete(model)
201
+ DATA.reject! { |d| d[:id].to_s == model.id.to_s }
202
+ model
203
+ end
204
+ ```
205
+
206
+ These are the overrides for persistence operations. You are encouraged
207
+ **not** to override `create/update/destroy` directly and instead use
208
+ [Persistence Lifecycle Hooks](/concepts/persisting#persistence-lifecycle-hooks).
209
+
210
+ ## Adapters {#adapters}
211
+
212
+ OK so we have all our read and write operations working correctly. But
213
+ if we had multiple Resources all using an in-memory datastore, you'd see
214
+ this logic repeated all over the place. Let's create an adapter to [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself)
215
+ up this logic.
216
+
217
+ There isn't much more to do than copy/paste what we've already done.
218
+ Let's start with our `base_scope`, sorting, and pagination:
219
+
220
+ ```ruby
221
+ class POROAdapter < Graphiti::Adapters::Abstract
222
+ def base_scope(*)
223
+ { sort: {}, filters: {} }
224
+ end
225
+
226
+ def paginate(scope, current, per)
227
+ scope.merge!(current_page: current, per_page: per)
228
+ end
229
+
230
+ def order(scope, att, dir)
231
+ scope[:sort].merge!(attribute: att, direction: dir)
232
+ scope
233
+ end
234
+
235
+ def resolve(scope)
236
+ data = DATA
237
+ if sort = scope[:sort].presence
238
+ data = data.sort_by { |d| d[sort[:attribute].to_sym] }
239
+ data = data.reverse if sort[:direction] == :desc
240
+ end
241
+ start = (scope[:current_page] - 1) * scope[:per_page]
242
+ stop = start + scope[:per_page]
243
+ data = data[start...stop]
244
+
245
+ data.map { |d| resource.model.new(d) }
246
+ end
247
+ end
248
+ ```
249
+
250
+ There's really nothing here we haven't seen before. We're taking the
251
+ code we originally wrote, and sticking it into the interface defined by
252
+ `Graphiti::Adapters::Abstract`.
253
+
254
+ There's a *little* more to do with filtering:
255
+
256
+ ```ruby
257
+ def filter(scope, attribute, value)
258
+ scope[:filters][attribute] = value
259
+ scope
260
+ end
261
+ alias :filter_string_eq :filter
262
+ alias :filter_integer_eq :filter
263
+ alias :filter_date_eq :filter
264
+ # ... etc ...
265
+ ```
266
+
267
+ The logic is the same, but we have a separate method for each filter
268
+ operator. This allows us to query differently based on the type - for
269
+ instance, ActiveRecord will default to case-insensitive for strings, but
270
+ straight equality for integers. If you don't need operator-specific
271
+ logic, just `alias` as you see here.
272
+
273
+ You may want to limit the default operators we expect to work with a
274
+ given type. Let's say your backend allows straight equality for strings,
275
+ but doesn't support `prefix`, `suffix`, etc. You can specify this in
276
+ your adapter:
277
+
278
+ ```ruby
279
+ def self.default_operators
280
+ super.tap do |built_in|
281
+ built_in[:string] = [:eq]
282
+ end
283
+ end
284
+
285
+ # or avoid super altogether
286
+
287
+ def self.default_operators
288
+ {
289
+ string: [:eq],
290
+ integer: [:eq]
291
+ # ... etc ...
292
+ }
293
+ end
294
+ ```
295
+
296
+ **That's it for reads**. For writes, I'll post the entire adapter code
297
+ below - again, it's just copy/pasting what we already wrote into a
298
+ slightly different format.
299
+
300
+ ```ruby
301
+ def destroy(model)
302
+ Post::DATA.reject! { |d| d[:id].to_s == model.id.to_s }
303
+ model
304
+ end
305
+
306
+ def save(model)
307
+ attrs = model.attributes.dup
308
+ attrs[:id] ||= Post::DATA.length + 1
309
+ if existing = Post::DATA.find { |d| d[:id].to_s == attrs[:id].to_s }
310
+ existing.merge!(attrs)
311
+ else
312
+ Post::DATA << attrs
313
+ end
314
+ model
315
+ end
316
+
317
+ # For wrapping persistence operations in a DB transactions
318
+ # Our in-memory DB doesn't have transactions, so just yield
319
+ def transaction(*)
320
+ yield
321
+ end
322
+ ```
323
+
324
+ That's really it. [See the working code in Employee Directory here](https://github.com/graphiti-api/employee_directory/blob/poro/app/resources/post_resource.rb).
@@ -0,0 +1,58 @@
1
+ ---
2
+ title: 'Tutorial'
3
+ ---
4
+
5
+ <p align="center">
6
+ <img src="/assets/img/legacy/legacy-0c75a16b3a.gif" />
7
+ </p>
8
+
9
+ # Tutorial
10
+ This tutorial serves as a deeper-dive into Graphiti development,
11
+ building an Employee Directory application. We purposefully built this
12
+ to illustrate common - but non-trivial - scenarios present in many
13
+ applications.
14
+
15
+ You'll need Ruby 3.2+ and Rails 7.1+ installed. Step 0 starts from an empty directory, so nothing else is assumed.
16
+
17
+ A core concept of Graphiti is **Test-First** - the most pleasant way to
18
+ develop Graphiti is by starting with an [integration test](/topics/testing). But that can add a lot of noise to a tutorial like this. Though we'll occasionally touch on testing - and the git diffs at the top of each section contain the necessary tests - we won't test first for the purposes of this tutorial.
19
+
20
+
21
+ ### Server Side: Rails
22
+
23
+ [Rails Sample Application](https://github.com/graphiti-api/employee_directory)
24
+
25
+ * [Step 0: Bootstrapping](/tutorial/step_0)
26
+ * [Step 1: Initial Resource](/tutorial/step_1)
27
+ * [Step 2: Has Many](/tutorial/step_2)
28
+ * [Step 3: Belongs To](/tutorial/step_3)
29
+ * [Step 4: Customizing Queries](/tutorial/step_4)
30
+ * [Step 5: Has One](/tutorial/step_5)
31
+ * [Step 6: Customizing Writes](/tutorial/step_6)
32
+ * [Step 7: Many-to-Many](/tutorial/step_7)
33
+ * [Step 8: Polymorphic
34
+ Relationships](/tutorial/step_8)
35
+ * [Step 9: Polymorphic Resources](/tutorial/step_9)
36
+
37
+
38
+
39
+ ### Client Side: VueJS (diff-only)
40
+
41
+ [VueJS Sample Application](https://github.com/graphiti-api/employee-directory-vue)
42
+
43
+
44
+ * [Step 0: Setup](https://github.com/graphiti-api/employee-directory-vue/commit/be690c3038380e17e326935d595a0b83fc8004f9)
45
+ * Run after `vue create employee-directory-vue` using [Vue CLI](https://cli.vuejs.org).
46
+ * [Step 1: Define Models](https://github.com/graphiti-api/employee-directory-vue/compare/step_0_setup...step_1_models)
47
+ * [Step 2: Data Grid](https://github.com/graphiti-api/employee-directory-vue/compare/step_1_models...step_2_data_grid)
48
+ * [Step 3: Relationships](https://github.com/graphiti-api/employee-directory-vue/compare/step_2_data_grid...step_3_includes)
49
+ * [Step 4: Filtering](https://github.com/graphiti-api/employee-directory-vue/compare/step_3_includes...step_4_filtering)
50
+ * [Step 5: Sorting](https://github.com/graphiti-api/employee-directory-vue/compare/step_4_filtering...step_5_sorting)
51
+ * [Step 6: Total Count](https://github.com/graphiti-api/employee-directory-vue/compare/step_5_sorting...step_6_stats)
52
+ * [Step 7: Pagination](https://github.com/graphiti-api/employee-directory-vue/compare/step_6_stats...step_7_pagination)
53
+ * [Step 8: Basic Form Setup](https://github.com/graphiti-api/employee-directory-vue/compare/step_7_pagination...step_8_basic_form_setup)
54
+ * [Step 9: Dropdown](https://github.com/graphiti-api/employee-directory-vue/compare/step_8_basic_form_setup...step_9_dropdown)
55
+ * [Step 10: Nested Form Submission](https://github.com/graphiti-api/employee-directory-vue/compare/step_9_dropdown...step_10_nested_create)
56
+ * [Step 11: Validation Errors](https://github.com/graphiti-api/employee-directory-vue/compare/step_10_nested_create...step_11_validations)
57
+ * [Step 12: Nested Destroy](https://github.com/graphiti-api/employee-directory-vue/compare/step_11_validations...step_12_nested_destroy)
58
+ * [Step 13: Vue-Specific Glue Code](https://github.com/graphiti-api/employee-directory-vue/compare/step_12_nested_destroy...step_13_vue)
@@ -0,0 +1,107 @@
1
+ ---
2
+ title: 'Step 0'
3
+ ---
4
+
5
+ ## Step 0: Bootstrapping
6
+
7
+ > [View the Code](https://github.com/graphiti-api/employee_directory/commit/e2552ce212c68b41a3eb8161deb822fff3e159d6)
8
+
9
+ Let's start by creating a new Rails project. For help with an existing
10
+ project, check out [Installation: From
11
+ Scratch](/getting-started/installation).
12
+
13
+ We'll use the `-m` option to install from a template, which will add a few gems and apply some setup boilerplate. Accept all the default options.
14
+
15
+ ```bash
16
+ $ rails new employee_directory --api -m https://raw.githubusercontent.com/graphiti-api/graphiti_rails_template/master/all.rb
17
+ $ cd employee_directory
18
+ ```
19
+
20
+ > Note: if a network issue prevents you from pointing to this URL directly, you can download the file and and run this command as `-m /path/to/template`
21
+
22
+ Feel free to run `git diff` to see what the generator did, otherwise commit the result. You can now head to [Step 1: Basic Resource](/tutorial/step_1), or continue reading to better understand the code.
23
+
24
+ #### Digging Deeper 🧐
25
+
26
+ You'll see some boilerplate in `config/routes.rb`:
27
+
28
+ ```ruby
29
+ scope path: ApplicationResource.endpoint_namespace, defaults: { format: :jsonapi } do
30
+ # your routes go here
31
+ end
32
+ ```
33
+
34
+ This tells Rails that our API routes will be be prefixed - `/api/v1` by default. It also says that if no extension is in the URL (`.json`, `.xml`, etc), default
35
+ to the [JSONAPI Specification](http://jsonapi.org).
36
+
37
+ Let's look at the above `ApplicationResource`:
38
+
39
+ ```ruby
40
+ class ApplicationResource < Graphiti::Resource
41
+ self.abstract_class = true
42
+
43
+ # We'll be using ActiveRecord
44
+ self.adapter = Graphiti::Adapters::ActiveRecord
45
+
46
+ # Links are generated from base_url + endpoint_namespace
47
+ self.base_url = Rails.application.routes
48
+ .default_url_options[:host]
49
+ self.endpoint_namespace = '/api/v1'
50
+ end
51
+ ```
52
+
53
+ This should be pretty self-explanatory except for
54
+
55
+ ```ruby
56
+ self.base_url = Rails.application.routes
57
+ .default_url_options[:host]
58
+ ```
59
+
60
+ This is configured in `config/application.rb`:
61
+
62
+ ```ruby
63
+ module EmployeeDirectory
64
+ class Application < Rails::Application
65
+ routes.default_url_options[:host] = ENV.fetch('HOST', 'http://localhost:3000')
66
+ # ... code ...
67
+ end
68
+ end
69
+ ```
70
+
71
+ When deriving and validating [Links](/concepts/links), we'll use the `HOST` variable if
72
+ present, falling back to the Rails development default of
73
+ `http://localhost:3000`. This means our Links will look like:
74
+
75
+ ```ruby
76
+ "#{ENV['HOST']}/#{ApplicationRecord.endpoint_namespace}/#{Resource.type}"
77
+ ```
78
+
79
+ For example:
80
+
81
+ ```ruby
82
+ http://my-website.com/api/v1/employees
83
+ ```
84
+
85
+ Read more in the [Links Guide](/concepts/links).
86
+
87
+ Finally, there's some boilerplate in `ApplicationController`:
88
+
89
+ ```ruby
90
+ class ApplicationController < ActionController::API
91
+ # Support JSON, XML, JSON:API
92
+ include Graphiti::Rails::Responders
93
+ end
94
+ ```
95
+
96
+ This gets `respond_with` working, via the [Responders gem](https://github.com/plataformatec/responders). To render simple nested JSON like default Rails, we'll only need to add `.json` to the URL.
97
+
98
+ That's it for basic setup!
99
+
100
+
101
+ <h2 id="next">
102
+ <a href="/tutorial/step_1">
103
+ NEXT -
104
+ <small>Step 1: Basic Resource</small>
105
+ &raquo;
106
+ </a>
107
+ </h2>
@@ -0,0 +1,199 @@
1
+ ---
2
+ title: 'Step 1'
3
+ ---
4
+
5
+ ## Step 1: Basic Resource
6
+
7
+ > [View the Diff](https://github.com/graphiti-api/employee_directory/commit/45c1c92e14fb1c3a47b8ed246ceb2cba50e97c72)
8
+
9
+ We'll be working with a single database table, `employees`:
10
+
11
+ <table class="table table-small">
12
+ <thead>
13
+ <tr>
14
+ <th>id</th>
15
+ <th>first_name</th>
16
+ <th>last_name</th>
17
+ <th>age</th>
18
+ <th>created_at</th>
19
+ <th>updated_at</th>
20
+ </tr>
21
+ </thead>
22
+ <tbody>
23
+ <tr>
24
+ <td>1</td>
25
+ <td>Homer</td>
26
+ <td>Simpson</td>
27
+ <td>39</td>
28
+ <td>2018-09-04</td>
29
+ <td>2018-09-04</td>
30
+ </tr>
31
+ <tr>
32
+ <td>2</td>
33
+ <td>Waylon</td>
34
+ <td>Smithers</td>
35
+ <td>65</td>
36
+ <td>2018-09-04</td>
37
+ <td>2018-09-04</td>
38
+ </tr>
39
+ <tr>
40
+ <td>3</td>
41
+ <td>Monty</td>
42
+ <td>Burns</td>
43
+ <td>123</td>
44
+ <td>2018-09-04</td>
45
+ <td>2018-09-04</td>
46
+ </tr>
47
+ </tbody>
48
+ </table>
49
+
50
+ ### The Rails Stuff 🚂
51
+
52
+ Use the built-in generator to create the database table
53
+ and corresponding `ActiveRecord` model:
54
+
55
+ ```bash
56
+ $ bin/rails g model Employee first_name:string last_name:string age:integer
57
+ $ bin/rails db:migrate
58
+ ```
59
+
60
+ Now let's seed some random development data, using [Faker](https://github.com/stympy/faker) (which was installed in [Step 0](/tutorial/step_0)):
61
+
62
+ ```ruby
63
+ # db/seeds.rb
64
+ Employee.delete_all # Ensure the DB is cleaned each run
65
+
66
+ 100.times do
67
+ Employee.create! first_name: Faker::Name.first_name,
68
+ last_name: Faker::Name.last_name,
69
+ age: rand(20..80)
70
+ end
71
+ ```
72
+
73
+ Run this seed file with
74
+
75
+ ```bash
76
+ $ bin/rails db:seed
77
+ ```
78
+
79
+ ### The Graphiti Stuff 🎨
80
+
81
+ Just like Rails, Graphiti has built-in generators. Let's generate
82
+ the corresponding Resource for our `Employee` model:
83
+
84
+ ```bash
85
+ $ bin/rails g graphiti:resource Employee first_name:string last_name:string age:integer created_at:datetime updated_at:datetime
86
+ ```
87
+
88
+ This generated a few things, but for now let's focus on
89
+ `EmployeeResource`:
90
+
91
+ ```ruby
92
+ class EmployeeResource < ApplicationResource
93
+ attribute :first_name, :string
94
+ attribute :last_name, :string
95
+ attribute :age, :integer
96
+ attribute :created_at, :datetime, writable: false
97
+ attribute :updated_at, :datetime, writable: false
98
+ end
99
+ ```
100
+
101
+ This code defined the [RESTful Resource](https://restful-api-design.readthedocs.io/en/latest/resources.html) we want our API to expose. Let's run our server and see what it does:
102
+
103
+ ```bash
104
+ $ bin/rails s
105
+ ```
106
+
107
+ Visit `localhost:3000/api/v1/employees`. You should see a [JSONAPI Response](http://jsonapi.org):
108
+
109
+ <br />
110
+
111
+ ![jsonapi](/assets/img/legacy/legacy-0378a3bb39.png)
112
+
113
+ <br />
114
+
115
+ If you find the payload a little intimidating, add `.json` to the URL for a more traditional response, or `.xml` for XML. Both are different **renderings** of the same `EmployeeResource`.
116
+
117
+ `Resources` are comprised of `Attribute`s:
118
+
119
+ ```ruby
120
+ # app/resources/employee_resource.rb
121
+ attribute :first_name, :string
122
+ ```
123
+
124
+ Each attribute defines behavior for:
125
+
126
+ * Reading (display)
127
+ * Writing
128
+ * Sorting
129
+ * Filtering
130
+ * Fieldsets
131
+
132
+ Let's start with simple display, turning `first_name` into all capital
133
+ letters:
134
+
135
+ ```ruby
136
+ # app/resources/employee_resource.rb
137
+ attribute :first_name, :string do
138
+ # @object is your model instance
139
+ @object.first_name.upcase
140
+ end
141
+ ```
142
+
143
+ This is the most important thing to understand about Resources: they are just a collection of defaults, all of which can be overridden. `attribute :first_name` is shorthand for `attribute :first_name do @object.first_name end`.
144
+
145
+ We'll go into further Resource customizations over the course of this tutorial. For now, undo the capitalization change above, and verify our out-of-the-box defaults: the same filter, sort, and pagination capabilities you exercised in the [Quickstart](/getting-started/first-api#querying) work here too, just against `employees` instead of `posts`. See the [Overview guide](/concepts/overview) for the full capability reference.
146
+
147
+ Write operations are easiest to verify with integration tests, which were created when we generated our Resource: an **API Spec** covering the request/response cycle, and a **Resource Spec** covering the Resource's logic directly. See the [Testing Guide](/topics/testing) for what these look like and how they differ. The example there uses the same `create` payload shape the generator produced for `EmployeeResource`.
148
+
149
+ Before we run these specs, we need to edit our [factories](https://github.com/thoughtbot/factory_bot) to ensure
150
+ dynamic, randomized data. Let's change this:
151
+
152
+ ```ruby
153
+ # spec/factories/employee.rb
154
+
155
+ FactoryBot.define do
156
+ factory :employee do
157
+ first_name { "MyString" }
158
+ last_name { "MyString" }
159
+ age { 1 }
160
+ end
161
+ end
162
+ ```
163
+
164
+ To
165
+
166
+ ```ruby
167
+ # spec/factories/employee.rb
168
+
169
+ FactoryBot.define do
170
+ factory :employee do
171
+ first_name { Faker::Name.first_name }
172
+ last_name { Faker::Name.last_name }
173
+ age { rand(20..80) }
174
+ end
175
+ end
176
+ ```
177
+
178
+ Now run the generated specs:
179
+
180
+ ```bash
181
+ $ bundle exec rspec
182
+ ```
183
+
184
+ You'll see 11 tests pass, with 3 pending. One of the pending specs was
185
+ autogenerated by rails - you can delete `spec/models/employee_spec.rb`
186
+ for now.
187
+
188
+ That leaves us with two "update" specs. These are marked pending so you
189
+ can manage the data yourself. Follow the comments in these specs to add
190
+ attributes and get them passing.
191
+
192
+
193
+ <h2 id="next">
194
+ <a href="/tutorial/step_2">
195
+ NEXT -
196
+ <small>Step 2: Has Many</small>
197
+ &raquo;
198
+ </a>
199
+ </h2>