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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. checksums.yaml +4 -4
  2. data/.git-blame-ignore-revs +2 -0
  3. data/.github/workflows/ci.yml +55 -3
  4. data/.github/workflows/docs.yml +1 -1
  5. data/.github/workflows/notify-example-app.yml +18 -0
  6. data/.gitignore +2 -0
  7. data/.standard.yml +6 -0
  8. data/CHANGELOG.md +54 -0
  9. data/CONTRIBUTING.md +3 -1
  10. data/README.md +13 -206
  11. data/Rakefile +6 -3
  12. data/UPGRADING.md +1 -1
  13. data/graphiti.gemspec +8 -4
  14. data/lib/generators/graphiti/generator_mixin.rb +1 -1
  15. data/lib/generators/graphiti/install_generator.rb +1 -2
  16. data/lib/generators/graphiti/resource_generator.rb +6 -3
  17. data/lib/generators/graphiti/templates/application_resource.rb.erb +19 -0
  18. data/lib/generators/graphiti/templates/controller.rb.erb +26 -8
  19. data/lib/generators/graphiti/templates/index_request_spec.rb.erb +2 -2
  20. data/lib/generators/graphiti/templates/resource_reads_spec.rb.erb +4 -4
  21. data/lib/generators/graphiti/templates/show_request_spec.rb.erb +2 -2
  22. data/lib/graphiti/audit/report.rb +6 -6
  23. data/lib/graphiti/audit.rb +3 -4
  24. data/lib/graphiti/delegates/pagination.rb +2 -2
  25. data/lib/graphiti/deserializer.rb +3 -3
  26. data/lib/graphiti/errors.rb +6 -6
  27. data/lib/graphiti/extensions/temp_id.rb +1 -1
  28. data/lib/graphiti/query.rb +27 -8
  29. data/lib/graphiti/rails/context.rb +2 -1
  30. data/lib/graphiti/rails/controller.rb +3 -0
  31. data/lib/graphiti/rails/debugging.rb +1 -1
  32. data/lib/graphiti/rails/responders.rb +2 -1
  33. data/lib/graphiti/resource/configuration.rb +1 -0
  34. data/lib/graphiti/resource/dsl.rb +2 -2
  35. data/lib/graphiti/resource/interface.rb +3 -3
  36. data/lib/graphiti/resource/links.rb +1 -1
  37. data/lib/graphiti/resource/persistence.rb +1 -1
  38. data/lib/graphiti/resource/polymorphism.rb +1 -1
  39. data/lib/graphiti/resource.rb +2 -2
  40. data/lib/graphiti/resource_proxy.rb +0 -1
  41. data/lib/graphiti/runner.rb +8 -3
  42. data/lib/graphiti/scope.rb +13 -0
  43. data/lib/graphiti/scoping/filter.rb +5 -3
  44. data/lib/graphiti/scoping/paginate.rb +1 -1
  45. data/lib/graphiti/scoping/sort.rb +1 -1
  46. data/lib/graphiti/sideload/has_many.rb +1 -1
  47. data/lib/graphiti/spec_helpers/errors_proxy.rb +1 -1
  48. data/lib/graphiti/spec_helpers/matchers.rb +1 -1
  49. data/lib/graphiti/spec_helpers.rb +5 -0
  50. data/lib/graphiti/types.rb +5 -1
  51. data/lib/graphiti/util/cache_debug.rb +3 -3
  52. data/lib/graphiti/util/hash.rb +1 -1
  53. data/lib/graphiti/util/persistence.rb +6 -6
  54. data/lib/graphiti/util/serializer_relationships.rb +1 -1
  55. data/lib/graphiti/version.rb +1 -1
  56. data/lib/tasks/graphiti.rake +1 -1
  57. data/package.json +1 -0
  58. metadata +31 -197
  59. data/docs/concepts/backends-and-models.md +0 -122
  60. data/docs/concepts/endpoints.md +0 -183
  61. data/docs/concepts/links.md +0 -212
  62. data/docs/concepts/overview.md +0 -80
  63. data/docs/concepts/persisting.md +0 -376
  64. data/docs/concepts/relationships.md +0 -627
  65. data/docs/concepts/resources.md +0 -677
  66. data/docs/getting-started/first-api.md +0 -289
  67. data/docs/getting-started/installation.md +0 -185
  68. data/docs/intro.md +0 -307
  69. data/docs/js/authentication.md +0 -63
  70. data/docs/js/ddau.md +0 -20
  71. data/docs/js/extra-params.md +0 -41
  72. data/docs/js/index.md +0 -112
  73. data/docs/js/installation.md +0 -120
  74. data/docs/js/middleware.md +0 -72
  75. data/docs/js/models.md +0 -202
  76. data/docs/js/reads.md +0 -494
  77. data/docs/js/state-syncing.md +0 -100
  78. data/docs/js/writes.md +0 -373
  79. data/docs/reference/vandal.md +0 -63
  80. data/docs/reference/why.md +0 -13
  81. data/docs/topics/authorization.md +0 -155
  82. data/docs/topics/caching.md +0 -55
  83. data/docs/topics/customizing-sideloads.md +0 -156
  84. data/docs/topics/debugging.md +0 -242
  85. data/docs/topics/error-handling.md +0 -210
  86. data/docs/topics/etags.md +0 -46
  87. data/docs/topics/hopping-relationships.md +0 -149
  88. data/docs/topics/json-attributes.md +0 -77
  89. data/docs/topics/openstruct-models.md +0 -50
  90. data/docs/topics/remote-resources.md +0 -291
  91. data/docs/topics/testing.md +0 -894
  92. data/docs/topics/without-activerecord.md +0 -324
  93. data/docs/tutorial/index.md +0 -58
  94. data/docs/tutorial/step_0.md +0 -107
  95. data/docs/tutorial/step_1.md +0 -199
  96. data/docs/tutorial/step_2.md +0 -312
  97. data/docs/tutorial/step_3.md +0 -142
  98. data/docs/tutorial/step_4.md +0 -135
  99. data/docs/tutorial/step_5.md +0 -69
  100. data/docs/tutorial/step_6.md +0 -82
  101. data/docs/tutorial/step_7.md +0 -205
  102. data/docs/tutorial/step_8.md +0 -128
  103. data/docs/tutorial/step_9.md +0 -171
  104. data/docs/upgrading.md +0 -266
  105. data/website/.gitignore +0 -20
  106. data/website/README.md +0 -43
  107. data/website/docusaurus.config.js +0 -197
  108. data/website/package-lock.json +0 -19474
  109. data/website/package.json +0 -46
  110. data/website/sidebars.js +0 -82
  111. data/website/src/css/custom.css +0 -85
  112. data/website/src/pages/markdown-page.mdx +0 -7
  113. data/website/static/.nojekyll +0 -0
  114. data/website/static/1.13/2019/03/31/graphiti-1-0.html +0 -205
  115. data/website/static/1.13/2019/05/08/graphiti-1-1.html +0 -212
  116. data/website/static/1.13/2019/05/20/graphiti-1-2.html +0 -214
  117. data/website/static/1.13/2019/10/14/tutorial.html +0 -198
  118. data/website/static/1.13/CNAME +0 -1
  119. data/website/static/1.13/README.md +0 -16
  120. data/website/static/1.13/assets/css/syntax.css +0 -60
  121. data/website/static/1.13/assets/favicons/android-chrome-192x192.png +0 -0
  122. data/website/static/1.13/assets/favicons/android-chrome-256x256.png +0 -0
  123. data/website/static/1.13/assets/favicons/apple-touch-icon.png +0 -0
  124. data/website/static/1.13/assets/favicons/browserconfig.xml +0 -9
  125. data/website/static/1.13/assets/favicons/favicon-16x16.png +0 -0
  126. data/website/static/1.13/assets/favicons/favicon-32x32.png +0 -0
  127. data/website/static/1.13/assets/favicons/favicon.ico +0 -0
  128. data/website/static/1.13/assets/favicons/mstile-150x150.png +0 -0
  129. data/website/static/1.13/assets/favicons/safari-pinned-tab.svg +0 -1
  130. data/website/static/1.13/assets/favicons/site.webmanifest +0 -19
  131. data/website/static/1.13/assets/img/backend.gif +0 -0
  132. data/website/static/1.13/assets/img/conformity.png +0 -0
  133. data/website/static/1.13/assets/img/error_payload.png +0 -0
  134. data/website/static/1.13/assets/img/fancy-cushion.png +0 -0
  135. data/website/static/1.13/assets/img/gh.png +0 -0
  136. data/website/static/1.13/assets/img/home-bg.jpg +0 -0
  137. data/website/static/1.13/assets/img/lifecycle.gif +0 -0
  138. data/website/static/1.13/assets/img/logo-500.png +0 -0
  139. data/website/static/1.13/assets/img/logo.png +0 -0
  140. data/website/static/1.13/assets/img/love-graffiti.jpg +0 -0
  141. data/website/static/1.13/assets/img/meta_total_count.png +0 -0
  142. data/website/static/1.13/assets/img/persist.jpg +0 -0
  143. data/website/static/1.13/assets/img/resource.gif +0 -0
  144. data/website/static/1.13/assets/img/rest-graffiti.jpg +0 -0
  145. data/website/static/1.13/assets/img/rest1.gif +0 -0
  146. data/website/static/1.13/assets/img/rest2.gif +0 -0
  147. data/website/static/1.13/assets/img/rest3.gif +0 -0
  148. data/website/static/1.13/assets/img/rethink-rest-graffiti.jpg +0 -0
  149. data/website/static/1.13/assets/img/sunrise.png +0 -0
  150. data/website/static/1.13/assets/img/why.png +0 -0
  151. data/website/static/1.13/assets/js/highlight.pack.js +0 -2
  152. data/website/static/1.13/assets/main.css +0 -15518
  153. data/website/static/1.13/assets/main.css.map +0 -1
  154. data/website/static/1.13/bin/bundle +0 -109
  155. data/website/static/1.13/bin/jekyll +0 -27
  156. data/website/static/1.13/bin/kramdown +0 -27
  157. data/website/static/1.13/bin/listen +0 -27
  158. data/website/static/1.13/bin/rake +0 -27
  159. data/website/static/1.13/bin/rougify +0 -27
  160. data/website/static/1.13/bin/safe_yaml +0 -27
  161. data/website/static/1.13/bin/sass +0 -27
  162. data/website/static/1.13/bin/sass-convert +0 -27
  163. data/website/static/1.13/bin/scss +0 -27
  164. data/website/static/1.13/blog.html +0 -259
  165. data/website/static/1.13/cheatsheet.html +0 -316
  166. data/website/static/1.13/cookbooks/authorization.md +0 -0
  167. data/website/static/1.13/cookbooks/caching.md +0 -0
  168. data/website/static/1.13/cookbooks/customizing-sideloads.html +0 -325
  169. data/website/static/1.13/cookbooks/etags.md +0 -0
  170. data/website/static/1.13/cookbooks/hopping-relationships.html +0 -324
  171. data/website/static/1.13/cookbooks/json_attributes.md +0 -0
  172. data/website/static/1.13/cookbooks/openstruct-models.md +0 -0
  173. data/website/static/1.13/cookbooks/remote-resources.md +0 -0
  174. data/website/static/1.13/cookbooks/without-activerecord.html +0 -510
  175. data/website/static/1.13/features.html +0 -249
  176. data/website/static/1.13/feed.xml +0 -106
  177. data/website/static/1.13/guides/concepts/backends-and-models.html +0 -467
  178. data/website/static/1.13/guides/concepts/debugging.html +0 -440
  179. data/website/static/1.13/guides/concepts/endpoints.html +0 -432
  180. data/website/static/1.13/guides/concepts/error-handling.html +0 -396
  181. data/website/static/1.13/guides/concepts/links.html +0 -501
  182. data/website/static/1.13/guides/concepts/remote-resources.html +0 -536
  183. data/website/static/1.13/guides/concepts/resources.html +0 -2176
  184. data/website/static/1.13/guides/concepts/testing.html +0 -1469
  185. data/website/static/1.13/guides/getting-started/installation.html +0 -420
  186. data/website/static/1.13/guides/graphiti-rails-migration.html +0 -242
  187. data/website/static/1.13/guides/index.html +0 -269
  188. data/website/static/1.13/guides/overview.html +0 -325
  189. data/website/static/1.13/guides/upgrading-2-0.html +0 -193
  190. data/website/static/1.13/guides/upgrading.html +0 -314
  191. data/website/static/1.13/guides/vandal.html +0 -282
  192. data/website/static/1.13/guides/why.html +0 -1121
  193. data/website/static/1.13/index.html +0 -72
  194. data/website/static/1.13/js/authentication.html +0 -295
  195. data/website/static/1.13/js/ddau.html +0 -238
  196. data/website/static/1.13/js/extra-params.html +0 -270
  197. data/website/static/1.13/js/index.html +0 -321
  198. data/website/static/1.13/js/installation.html +0 -637
  199. data/website/static/1.13/js/introduction.html +0 -257
  200. data/website/static/1.13/js/middleware.html +0 -318
  201. data/website/static/1.13/js/reads/fieldsets.html +0 -271
  202. data/website/static/1.13/js/reads/filtering.html +0 -289
  203. data/website/static/1.13/js/reads/includes.html +0 -260
  204. data/website/static/1.13/js/reads/index.html +0 -497
  205. data/website/static/1.13/js/reads/nested-queries.html +0 -353
  206. data/website/static/1.13/js/reads/pagination.html +0 -260
  207. data/website/static/1.13/js/reads/sorting.html +0 -265
  208. data/website/static/1.13/js/reads/statistics.html +0 -289
  209. data/website/static/1.13/js/state-syncing.html +0 -340
  210. data/website/static/1.13/js/writes/deferred.html +0 -296
  211. data/website/static/1.13/js/writes/dirty-tracking.html +0 -399
  212. data/website/static/1.13/js/writes/index.html +0 -391
  213. data/website/static/1.13/js/writes/nested.html +0 -330
  214. data/website/static/1.13/js/writes/validations.html +0 -272
  215. data/website/static/1.13/quickstart.html +0 -660
  216. data/website/static/1.13/template +0 -161
  217. data/website/static/1.13/tutorial/index.html +0 -250
  218. data/website/static/1.13/tutorial/step_0.html +0 -292
  219. data/website/static/1.13/tutorial/step_1.html +0 -517
  220. data/website/static/1.13/tutorial/step_2.html +0 -481
  221. data/website/static/1.13/tutorial/step_3.html +0 -323
  222. data/website/static/1.13/tutorial/step_4.html +0 -318
  223. data/website/static/1.13/tutorial/step_5.html +0 -265
  224. data/website/static/1.13/tutorial/step_6.html +0 -276
  225. data/website/static/1.13/tutorial/step_7.html +0 -390
  226. data/website/static/1.13/tutorial/step_8.html +0 -316
  227. data/website/static/1.13/tutorial/step_9.html +0 -365
  228. data/website/static/CNAME +0 -1
  229. data/website/static/assets/img/error_payload.png +0 -0
  230. data/website/static/assets/img/legacy/legacy-0378a3bb39.png +0 -0
  231. data/website/static/assets/img/legacy/legacy-05bbd3e5fd.png +0 -0
  232. data/website/static/assets/img/legacy/legacy-07aa104495.png +0 -0
  233. data/website/static/assets/img/legacy/legacy-0c75a16b3a.gif +0 -0
  234. data/website/static/assets/img/legacy/legacy-3076df6209.png +0 -0
  235. data/website/static/assets/img/legacy/legacy-7f6889bc89.png +0 -0
  236. data/website/static/assets/img/legacy/legacy-a2cc4363c3.png +0 -0
  237. data/website/static/assets/img/legacy/legacy-f67cfa89ab.png +0 -0
  238. data/website/static/assets/img/meta_total_count.png +0 -0
  239. data/website/static/img/docusaurus-social-card.jpg +0 -0
  240. data/website/static/img/docusaurus.png +0 -0
  241. data/website/static/img/favicon.ico +0 -0
  242. data/website/static/img/logo.png +0 -0
  243. data/website/static/img/logo.svg +0 -1
  244. data/website/static/img/undraw_docusaurus_mountain.svg +0 -171
  245. data/website/static/img/undraw_docusaurus_react.svg +0 -170
  246. data/website/static/img/undraw_docusaurus_tree.svg +0 -40
@@ -1,677 +0,0 @@
1
- ---
2
- title: 'Resources'
3
- ---
4
-
5
- # Resources
6
-
7
- A Resource is an abstraction around an API endpoint, the way a Model is an abstraction around a database table. It holds the logic for **querying**, **persisting**, and **serializing** one kind of thing.
8
-
9
- ```ruby
10
- class EmployeeResource < ApplicationResource
11
- attribute :first_name, :string
12
- attribute :age, :integer
13
-
14
- has_many :positions
15
- end
16
- ```
17
-
18
- This page is the full reference. For the whole API on one screen, see the [cheatsheet on the home page](/). For how a request flows through a Resource, see [Lifecycle of a Request](/concepts/overview).
19
-
20
- Resources connect to each other. That's covered separately in [Relationships](/concepts/relationships), and writes in [Persisting](/concepts/persisting).
21
-
22
- ## Attributes {#attributes}
23
-
24
- ```ruby
25
- attribute :first_name, :string
26
- ```
27
-
28
- A **name** (`first_name`) maps to a JSON key. A **Type** (`string`) maps to a JSON value and its coercion rules.
29
-
30
- ### Limiting Behavior {#limiting-behavior}
31
-
32
- ```ruby
33
- attribute :name, :string,
34
- readable: true, # renders in responses
35
- writable: true, # accepted on create/update
36
- sortable: true, # ?sort=name works
37
- filterable: true, # ?filter[name]=... works
38
- schema: true # exported to schema.json, not affected by only/except
39
- ```
40
-
41
- Turn any flag off directly, or with `only`/`except` shorthand:
42
-
43
- ```ruby
44
- attribute :name, :string, sortable: false
45
- attribute :name, :string, only: [:sortable]
46
- attribute :name, :string, except: [:writable]
47
- ```
48
-
49
- **Guards.** `readable` and `writable` also accept a symbol, string, or proc. The behavior applies only when the guard returns `true`, and the guard's arity decides what it receives:
50
-
51
- ```ruby
52
- attribute :name, :string, writable: :admin?
53
- attribute :salary, :integer, readable: :visible?, writable: :salary_writable?
54
-
55
- def admin? # no arguments
56
- context.current_user.admin?
57
- end
58
-
59
- def visible?(model) # the model
60
- model.internal == false
61
- end
62
-
63
- def salary_writable?(model, attribute_name) # the model and the attribute name
64
- PolicyChecker.new(model).attribute_writable?(attribute_name)
65
- end
66
- ```
67
-
68
- The model is only looked up when a guard declares a parameter for it, so zero-argument guards cost nothing. On an update it's the persisted record. On a create, it's a new unsaved instance.
69
-
70
- | Guard returns `false` on | Result |
71
- | --- | --- |
72
- | `readable` | The attribute is omitted from the response. |
73
- | `writable` | The request is rejected with an `unwritable_attribute` validation error, before anything is persisted. |
74
-
75
- ### Default Behavior {#default-behavior}
76
-
77
- ```ruby
78
- # On ApplicationResource, affects every subclass
79
- self.attributes_readable_by_default = false # default true
80
- self.attributes_writable_by_default = false # default true
81
- self.attributes_filterable_by_default = false # default true
82
- self.attributes_sortable_by_default = false # default true
83
- self.attributes_schema_by_default = false # default true
84
- ```
85
-
86
- Each `*_by_default` setting can also be a guard symbol, delegating the check to a method. Useful for wiring every attribute through one authorization system:
87
-
88
- ```ruby
89
- self.attributes_readable_by_default = :attribute_readable?
90
-
91
- def attribute_readable?(model_instance, attribute_name)
92
- PolicyChecker.new(model_instance).attribute_readable?(attribute_name)
93
- end
94
- ```
95
-
96
- ### Customizing Display {#customizing-display}
97
-
98
- ```ruby
99
- attribute :name, :string do
100
- @object.name.upcase # @object is the model instance
101
- end
102
- ```
103
-
104
- ### Types {#types}
105
-
106
- | Type | Notes |
107
- | --- | --- |
108
- | `string` | |
109
- | `integer` | |
110
- | `integer_id` | Renders as a string, queries/persists as an integer. Default type for `id`. |
111
- | `uuid` | Like `string`, but only `eq`/`not_eq`, case-sensitive by default. |
112
- | `string_enum` | Like `string`, but only `eq`/`not_eq`/`eql`/`not_eql`, and requires `allow:`. |
113
- | `integer_enum` | Like `integer`, but only `eq`/`not_eq`, and requires `allow:`. |
114
- | `big_decimal` | |
115
- | `float` | |
116
- | `boolean` | |
117
- | `date` | |
118
- | `datetime` | |
119
- | `hash` | |
120
- | `array` | |
121
-
122
- Every type except `boolean`, `hash`, and `array` also has an `array_of_*` variant: `array_of_integers`, `array_of_dates`, `array_of_uuids`, and so on.
123
-
124
- Each Type governs reading, writing, and filtering by wrapping a [Dry Type](https://dry-rb.org/gems/dry-types). Inspect one to see its parts:
125
-
126
- ```ruby
127
- Graphiti::Types[:integer_id]
128
-
129
- # {
130
- # params: Dry::Types['coercible.integer'],
131
- # read: Dry::Types['coercible.string'],
132
- # write: Dry::Types['coercible.integer'],
133
- # ...
134
- # }
135
- ```
136
-
137
- Edit an implementation in place. Here, `:string` is made to render as an integer:
138
-
139
- ```ruby
140
- Graphiti::Types[:string][:read] = Dry::Types['coercible.integer']
141
- ```
142
-
143
- #### Enum Types {#enum-types}
144
-
145
- `string_enum` and `integer_enum` behave like `string` and `integer`, except declaring one (as an attribute or a filter) requires the `allow:` option, the list of acceptable values:
146
-
147
- ```ruby
148
- attribute :status, :string_enum, allow: ['draft', 'published']
149
- ```
150
-
151
- If your attribute is backed by an ActiveRecord enum, reference the values directly:
152
-
153
- ```ruby
154
- # app/models/post.rb
155
- class Post < ApplicationRecord
156
- enum status: {
157
- draft: 0,
158
- published: 1
159
- }
160
- end
161
-
162
- # app/resources/post_resource.rb
163
- class PostResource < ApplicationResource
164
- attribute :status, :string_enum, allow: Post.statuses.keys
165
- end
166
- ```
167
-
168
- See [Filter Options](#filter-options) for more on `allow`.
169
-
170
- Graphiti does not validate enum values on write. Your model layer is still expected to validate incoming data.
171
-
172
- #### Custom Types {#custom-types}
173
-
174
- [Dry Types supports custom types](https://dry-rb.org/gems/dry-types/main/custom-types/):
175
-
176
- ```ruby
177
- # Define the Type
178
- definition = Dry::Types::Nominal.new(String)
179
- type = definition.constructor do |input|
180
- input.upcase
181
- end
182
-
183
- # Register it with Graphiti
184
- Graphiti::Types[:caps_lock] = {
185
- params: type,
186
- read: type,
187
- write: type,
188
- kind: 'scalar',
189
- canonical_name: :caps_lock,
190
- description: 'All capital letters'
191
- }
192
-
193
- # Use in a Resource
194
- attribute :name, :caps_lock
195
- ```
196
-
197
- ## Querying {#querying}
198
-
199
- ```ruby
200
- class PostResource < ApplicationResource
201
- # Applies to every query: start with a base scope, alter it based on
202
- # the incoming request. Called just like ActiveRecord's Post.all.
203
- def base_scope
204
- Post.all
205
- end
206
-
207
- # Must execute the query and return an array of Model instances.
208
- def resolve(scope)
209
- scope.to_a
210
- end
211
- end
212
- ```
213
-
214
- ### Query Interface {#query-interface}
215
-
216
- Resources can query and persist without an API request or response. Pass a [JSONAPI-compliant](http://jsonapi.org) query hash directly:
217
-
218
- ```ruby
219
- EmployeeResource.all({
220
- filter: { first_name: 'Jane' },
221
- sort: '-created_at',
222
- page: { size: 10, number: 2 }
223
- })
224
- ```
225
-
226
- The return value from `.all` is a **proxy** object, similar to `ActiveRecord::Relation`. No query fires until you call `.map`, `.data`, or a render method:
227
-
228
- ```ruby
229
- employees = EmployeeResource.all
230
- employees.class # Graphiti::ResourceProxy
231
- employees.map(&:first_name) # => ["Jane", "Joe", ...]
232
- employees.data # => [#<Employee>, #<Employee>, ...]
233
-
234
- employees.to_jsonapi
235
- employees.to_json
236
- employees.to_xml
237
- ```
238
-
239
- `.find` returns a single record's proxy by id, raising `Graphiti::Errors::RecordNotFound` if none are returned:
240
-
241
- ```ruby
242
- employee = EmployeeResource.find(id: 123)
243
- employee.data.first_name # => "Jane"
244
- ```
245
-
246
- ### Composing with Scopes {#composing-with-scopes}
247
-
248
- #### #base_scope {#base-scope}
249
-
250
- ```ruby
251
- def base_scope
252
- Position.where(active: true)
253
- end
254
- ```
255
-
256
- Override `#base_scope` for logic that should apply to every query. Here, it only ever returns active Positions.
257
-
258
- Pass a second argument to `.all` to override the base scope for a single call:
259
-
260
- ```ruby
261
- class InactivePostsController < PostsController
262
- def index
263
- posts = PostResource.all(params, Post.where(active: false))
264
- respond_with(posts)
265
- end
266
- end
267
- ```
268
-
269
- ### Sort {#sort}
270
-
271
- ```ruby
272
- sort :name, :string do |scope, direction|
273
- scope.order(first_name: direction, last_name: direction)
274
- end
275
- ```
276
-
277
- Omit the type if a matching `attribute` is already defined. This overrides its default sort behavior:
278
-
279
- ```ruby
280
- attribute :name, :string
281
-
282
- sort :name do |scope, direction|
283
- # ... code ...
284
- end
285
- ```
286
-
287
- `sort` on its own defines a sort-only attribute. Define the `attribute` first if you also need filtering or other behavior.
288
-
289
- #### Sort Options {#sort-options}
290
-
291
- | Option | Description |
292
- | --- | --- |
293
- | `only` | Restrict to a single direction, e.g. `sort :name, only: [:desc]` |
294
-
295
- ### Filter {#filter}
296
-
297
- ```ruby
298
- filter :name, :string do
299
- eq do |scope, value|
300
- scope.where(first_name: value)
301
- end
302
-
303
- # prefix do ... end
304
- # suffix do ... end
305
- # etc
306
- end
307
- ```
308
-
309
- Omit the type if a matching `attribute` is already defined. This overrides its default filter behavior. `filter` on its own defines a filter-only attribute. Define the `attribute` first if you also need sorting or other behavior.
310
-
311
- Every operator below also has a `not_` counterpart (`not_eq`, `not_prefix`, ...). Values arrive as an array unless the filter is `single: true`. Comma-delimit multiple values in a query string (`/employees?filter[name]=Jane,John`).
312
-
313
- | Type | Default operators |
314
- | --- | --- |
315
- | `string` | `eq`, `eql`, `prefix`, `suffix`, `match` |
316
- | `uuid` | `eq` |
317
- | `string_enum`, `integer_enum` | `eq`, `eql` |
318
- | `integer_id`, `integer`, `big_decimal`, `float`, `date`, `datetime` | `eq`, `gt`, `gte`, `lt`, `lte` |
319
- | `boolean` | `eq` (always `single: true`) |
320
- | `hash` | `eq` |
321
- | `array` | `eq` |
322
-
323
- Define custom operators on the fly:
324
-
325
- ```ruby
326
- filter :name do
327
- fuzzy_match do |scope, value|
328
- # ... code ...
329
- end
330
- end
331
- ```
332
-
333
- This supports `filter[name][fuzzy_match]=foo`.
334
-
335
- #### Filter Options {#filter-options}
336
-
337
- | Option | Description |
338
- | --- | --- |
339
- | `only`, `except` | Limit the operators generated from the type's defaults, e.g. `filter :name, :string, only: [:eq, :suffix]` |
340
- | `allow` | Only permit these values, e.g. `filter :size, :string, allow: ['Big', 'Medium', 'Small']` |
341
- | `deny` | Reject these values, e.g. `filter :size, :string, deny: ['X-Large']` |
342
- | `single` | Accept one value instead of an array. `boolean` filters are `single: true` by default. |
343
- | `required` | Reject the request if the filter is absent, e.g. `filter :customer_id, :string, required: true` (equivalently, `attribute :customer_id, :integer, filterable: :required`) |
344
- | `dependent` | Require other filters alongside this one, e.g. `filter :customer_id, :integer, dependent: [:customer_type]` paired with `filter :customer_type, :string, dependent: [:customer_id]`, so querying by id requires type, and vice versa |
345
- | `allow_nil` | Coerce an incoming `null` to Ruby `nil` instead of the string `"null"`. Default `false`. Set `self.filters_accept_nil_by_default = true` on a Resource to flip it for all of that Resource's filters. |
346
-
347
- ```ruby
348
- # Default behavior
349
- filter :name, :string do
350
- eq do |scope, value|
351
- value # => ["Jane"]
352
- end
353
- end
354
-
355
- # With single: true
356
- filter :name, :string, single: true do
357
- eq do |scope, value|
358
- value # => "Jane"
359
- end
360
- end
361
- ```
362
-
363
- #### Boolean Filter {#boolean-filter}
364
-
365
- Filters with type `boolean` are `single: true` by default. A boolean filter accepting multiple values doesn't make sense.
366
-
367
- #### Hash Filter {#hash-filter}
368
-
369
- Filters with type `hash` parse JSON automatically when passed in a URL query string:
370
-
371
- ```ruby
372
- # GET /employees?filter[metadata]={ "foo": 100 }
373
-
374
- filter :metadata, :hash do
375
- eq do |scope, value|
376
- value # => [{ "foo" => 100 }]
377
- end
378
- end
379
- ```
380
-
381
- #### Escaping Values {#escaping-values}
382
-
383
- By default, Graphiti parses a comma-delimited string as an array. Wrap a value in `{{curlies}}` to keep it intact, for a "keyword search" field that could itself contain a comma:
384
-
385
- ```ruby
386
- # GET /employees?filter[keywords]={{some,value}}
387
-
388
- filter :keywords, :string do
389
- eq do |scope, value|
390
- value # => "some,value"
391
- end
392
- end
393
- ```
394
-
395
- Or define an array explicitly instead of relying on comma-splitting:
396
-
397
- ```ruby
398
- # GET /employees?filter[keywords]=[some,value]
399
-
400
- filter :keywords, :string do
401
- eq do |scope, value|
402
- value # => ["some", "value"]
403
- end
404
- end
405
- ```
406
-
407
- A `single: true` filter skips array parsing entirely and escapes the value for you, filtering on the string as given.
408
-
409
- ### Statistics {#statistics}
410
-
411
- ```ruby
412
- stat total: [:count]
413
- stat rating: [:average]
414
- stat likes: [:sum]
415
- stat score: [:maximum]
416
-
417
- stat rating: [:average] do
418
- standard_deviation do |scope, attr|
419
- # your standard deviation code here
420
- end
421
- end
422
- ```
423
-
424
- Every Resource has a `total: :count` statistic by default. Statistics respect filtering but not pagination, so you can show a "Total Posts" count above a paginated grid without a second request:
425
-
426
- ```ruby
427
- PostResource.all({
428
- stats: { total: 'count' }
429
- })
430
- # GET /posts?stats[total]=count
431
- ```
432
-
433
- ```ruby
434
- {
435
- meta: {
436
- stats: {
437
- total: {
438
- count: 100
439
- }
440
- }
441
- }
442
- }
443
- ```
444
-
445
- ### Extra Fields {#extra-fields}
446
-
447
- ```ruby
448
- extra_attribute :net_worth
449
- ```
450
-
451
- Works like `attribute`, except the field is read-only and only returned when explicitly requested: `?extra_fields[employees]=net_worth`.
452
-
453
- Adjust the scope (e.g. to eager-load) only when the extra field is requested:
454
-
455
- ```ruby
456
- resource.on_extra_attribute :net_worth do |scope|
457
- scope.includes(:assets)
458
- end
459
- ```
460
-
461
- ### #resolve {#resolve}
462
-
463
- `#resolve` must execute the query and return an array of `Model` instances. Override it to add behavior around the default:
464
-
465
- ```ruby
466
- def resolve(scope)
467
- Rails.logger.info "begin resolving scope..."
468
- result = super
469
- Rails.logger.info "resolved!"
470
- result
471
- end
472
- ```
473
-
474
- ## Configuration {#configuration}
475
-
476
- ```ruby
477
- class PostResource < ApplicationResource
478
- self.model = Post
479
- self.type = 'posts'
480
-
481
- # Only used if you care about Links
482
- primary_endpoint '/posts', [:index, :show, :create, :update, :destroy]
483
-
484
- self.default_sort = [{ title: :asc }] # default nil
485
- self.default_page_size = 10 # default 20
486
- end
487
- ```
488
-
489
- Typically inherited from `ApplicationResource`, where cross-cutting settings live:
490
-
491
- ```ruby
492
- class ApplicationResource < Graphiti::Resource
493
- # Required when there's no corresponding model
494
- self.abstract_class = true
495
-
496
- # Subclasses override as needed
497
- self.adapter = Graphiti::Adapters::ActiveRecord
498
-
499
- # Default attribute flags. See #limiting-behavior
500
- self.attributes_readable_by_default = true
501
- self.attributes_writable_by_default = true
502
- self.attributes_sortable_by_default = true
503
- self.attributes_filterable_by_default = true
504
-
505
- # Used for link generation
506
- self.base_url = Rails.application.routes.default_url_options[:host]
507
- # Suggest referencing this in config/routes.rb:
508
- # scope path: ApplicationResource.endpoint_namespace do
509
- # resources :posts
510
- # end
511
- self.endpoint_namespace = '/api/v1'
512
-
513
- # Raise if a Resource is accessed from a URL it isn't allowlisted for
514
- self.validate_endpoints = false
515
-
516
- # Automatically generate JSONAPI links?
517
- self.autolink = true
518
- end
519
- ```
520
-
521
- ### Polymorphic Resources {#polymorphic-resources}
522
-
523
- Polymorphic Resources are similar to [ActiveRecord STI](https://api.rubyonrails.org/classes/ActiveRecord/Inheritance.html): a single query returns multiple Resource types. Querying `/tasks` can return `bugs`, `features`, and `epics`.
524
-
525
- ```ruby
526
- class Employee < ApplicationRecord
527
- has_many :tasks
528
- end
529
-
530
- # tasks table has a 'type' column
531
- class Task < ApplicationRecord
532
- belongs_to :employee
533
- end
534
-
535
- class Bug < Task
536
- end
537
-
538
- # ONLY Feature has #points
539
- class Feature < Task
540
- def points
541
- 5
542
- end
543
- end
544
-
545
- # ONLY Epic has the milestones relationship
546
- class Epic < Task
547
- has_many :milestones
548
- end
549
-
550
- class Milestone < ApplicationRecord
551
- belongs_to :epic
552
- end
553
- ```
554
-
555
- ```ruby
556
- class TaskResource < ApplicationResource
557
- # Reference child classes
558
- self.polymorphic = [
559
- 'BugResource',
560
- 'FeatureResource',
561
- 'EpicResource'
562
- ]
563
-
564
- attribute :title, :string
565
- end
566
-
567
- class BugResource < TaskResource
568
- end
569
-
570
- class FeatureResource < TaskResource
571
- attribute :points, :integer
572
- end
573
-
574
- class EpicResource < TaskResource
575
- has_many :milestones
576
- end
577
-
578
- class MilestoneResource < TaskResource
579
- belongs_to :epic
580
- end
581
- ```
582
-
583
- `/tasks` returns [JSONAPI types](http://jsonapi.org/format/#document-resource-identifier-objects) of `bugs`, `features`, and `epics`. Only `features` render `points`. Only `epics` render the `milestones` relationship. `/tasks?include=milestones` correctly only queries and renders Milestones for Epics.
584
-
585
- Resources connect to each other through relationships. See [Relationships](/concepts/relationships).
586
-
587
- ## Generators {#generators}
588
-
589
- ```bash
590
- $ rails generate graphiti:resource NAME [attribute:type] [options]
591
- ```
592
-
593
- ```bash
594
- $ rails generate graphiti:resource Employee first_name:string age:integer
595
- ```
596
-
597
- Adds a route, controller, resource, and tests.
598
-
599
- Limit the actions the resource supports with `-a`:
600
-
601
- ```bash
602
- $ rails generate graphiti:resource Employee -a index show
603
- ```
604
-
605
- Writing data (creating, updating, and destroying resources, including a graph of them in a single request) is covered in [Persisting](/concepts/persisting).
606
-
607
- ## Context {#context}
608
-
609
- ```ruby
610
- # app/resources/post_resource.rb
611
- attribute :active, :boolean, writable: :admin?
612
-
613
- def admin?
614
- context.current_user.admin?
615
- end
616
- ```
617
-
618
- Every Resource has access to `#context`. Under Rails, `context` is the controller instance processing the request.
619
-
620
- Put common helpers like `current_user` on `ApplicationResource`, so every Resource can call them:
621
-
622
- ```ruby
623
- # app/resources/application_resource.rb
624
- class ApplicationResource < Graphiti::Resource
625
- # ... code ...
626
- def current_user
627
- context.current_user
628
- end
629
- end
630
-
631
- # app/resources/post_resource.rb
632
- class PostResource < ApplicationResource
633
- # ... code ...
634
- def admin?
635
- current_user.admin?
636
- end
637
- end
638
- ```
639
-
640
- Set context manually with `with_context`:
641
-
642
- ```ruby
643
- ctx = OpenStruct.new(current_user: User.first)
644
- Graphiti.with_context(ctx) do
645
- # current_user == ctx.current_user
646
- PostResource.all
647
- end
648
- ```
649
-
650
- ## Concurrency {#concurrency}
651
-
652
- ```ruby
653
- # config/initializers/graphiti.rb
654
- Graphiti.configure do |c|
655
- c.concurrency = false
656
- end
657
- ```
658
-
659
- Under Rails, concurrency turns on by default when `::Rails.application.config.cache_classes` is `true` (the default for staging and production). Sibling sideloads then load concurrently, so a `Post` sideloading `Comments` and `Author` loads both at the same time.
660
-
661
- Concurrency runs sideloads in new Threads, so thread locals are dropped. Use `Graphiti.context` instead of `Thread.current` for anything that needs to survive a sideload:
662
-
663
- ```ruby
664
- # BAD:
665
- Thread.current[:foo] = "bar"
666
- Thread.current[:foo] # => will be nil when sideloading!
667
-
668
- # GOOD:
669
- Graphiti.context[:foo] = "bar"
670
- Graphiti.context[:foo] # => "bar", even when sideloading
671
- ```
672
-
673
- ## Adapters {#adapters}
674
-
675
- Common resource overrides can be packaged into an Adapter for code re-use, most commonly to use a different client/datastore than ActiveRecord/RelationalDB.
676
-
677
- [Adapters are best explained in the 'Without ActiveRecord' recipe](/topics/without-activerecord).