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
data/docs/intro.md ADDED
@@ -0,0 +1,307 @@
1
+ ---
2
+ id: intro
3
+ title: 'Graphiti'
4
+ sidebar_label: 'Overview'
5
+ sidebar_position: 0
6
+ slug: /
7
+ ---
8
+
9
+ # Graphiti
10
+
11
+ :::info These are the 2.0 docs
12
+ Graphiti 2.0 folds `graphiti-rails`, `graphiti_spec_helpers` and `graphiti_errors` into the main gem, and requires Ruby 3.2+ and ActiveSupport 7.1+. Rails is optional, and 7.1+ if you use it. Coming from 1.x? Start with the **[2.0 upgrade guide](/upgrading)**. Docs for 1.x are in the version picker.
13
+ :::
14
+
15
+ Graphiti sits on top of your models and exposes them over a [JSON:API](https://jsonapi.org)-compliant interface. You define Resources instead of controllers and serializers, and get filtering, sorting, pagination, sparse fieldsets, statistics, and nested reads and writes across relationships, all over one endpoint.
16
+
17
+ Here is the whole loop. A Resource declares what's exposed:
18
+
19
+ ```ruby title="app/resources/employee_resource.rb"
20
+ class EmployeeResource < ApplicationResource
21
+ attribute :first_name, :string
22
+ attribute :last_name, :string
23
+ attribute :age, :integer
24
+
25
+ has_many :positions
26
+ end
27
+ ```
28
+
29
+ The controller hands it the request params and renders the result:
30
+
31
+ ```ruby title="app/controllers/employees_controller.rb"
32
+ class EmployeesController < ApplicationController
33
+ def index
34
+ employees = EmployeeResource.all(params)
35
+
36
+ respond_to do |format|
37
+ format.jsonapi { render(jsonapi: employees) }
38
+ format.json { render(json: employees) }
39
+ format.xml { render(xml: employees) }
40
+ end
41
+ end
42
+
43
+ def show
44
+ employee = EmployeeResource.find(params)
45
+ authorize employee.data # pundit. .data is the model itself
46
+ render(jsonapi: employee)
47
+ end
48
+ end
49
+ ```
50
+
51
+ A client asks for employees and their positions in one request:
52
+
53
+ ```http title="Request"
54
+ GET /api/v1/employees?include=positions
55
+ ```
56
+
57
+ ```json title="Response"
58
+ {
59
+ "data": [
60
+ {
61
+ "id": "1",
62
+ "type": "employees",
63
+ "attributes": {
64
+ "first_name": "Jane",
65
+ "last_name": "Doe",
66
+ "age": 34
67
+ },
68
+ "relationships": {
69
+ "positions": {
70
+ "data": [
71
+ { "type": "positions", "id": "1" },
72
+ { "type": "positions", "id": "2" }
73
+ ]
74
+ }
75
+ }
76
+ }
77
+ ],
78
+ "included": [
79
+ {
80
+ "id": "1",
81
+ "type": "positions",
82
+ "attributes": { "title": "Engineer" }
83
+ },
84
+ {
85
+ "id": "2",
86
+ "type": "positions",
87
+ "attributes": { "title": "Senior Engineer" }
88
+ }
89
+ ]
90
+ }
91
+ ```
92
+
93
+ That same Resource also serves `?filter[age][gt]=30`, `?sort=-age`, `?page[size]=10`, `?fields[employees]=first_name`, and `?stats[total]=count`, without writing any of them.
94
+
95
+ The same proxy renders all three formats, so `/employees.jsonapi`, `/employees.json` and `/employees.xml` all work off one action.
96
+
97
+ `.all` and `.find` return that proxy, so nothing has been queried yet. `.data` is where you reach the model, and also where per-record authorization goes. See [Authorization](/topics/authorization#integrating-with-pundit).
98
+
99
+ If repeating that `respond_to` block gets old, the optional [`responders`](https://github.com/heartcombo/responders) integration collapses it to `respond_with(employees)`. See [Installation](/getting-started/installation#responders).
100
+
101
+ ## The whole Resource API
102
+
103
+ Every Resource is a collection of defaults, and you can override any of them. Below is one Resource with those defaults written out the long way, the entire surface area on a single page. You wouldn't write this much by hand. It's here so you can see what's available.
104
+
105
+ ### ApplicationResource
106
+
107
+ Every Resource inherits from an `ApplicationResource`, the same way models inherit from `ApplicationRecord`. This is where cross-cutting configuration lives, so individual Resources stay small. It's also the right place to put helpers like `current_user`, which guards throughout your API can then call.
108
+
109
+ ```ruby title="app/resources/application_resource.rb"
110
+ class ApplicationResource < Graphiti::Resource
111
+ # Required when there's no corresponding model
112
+ self.abstract_class = true
113
+
114
+ # Subclasses override as needed
115
+ self.adapter = Graphiti::Adapters::ActiveRecord
116
+
117
+ # Flip any of these to lock down every Resource at once,
118
+ # e.g. a read-only API
119
+ self.attributes_readable_by_default = true
120
+ self.attributes_writable_by_default = true
121
+ self.attributes_sortable_by_default = true
122
+ self.attributes_filterable_by_default = true
123
+
124
+ # Used for link generation
125
+ self.base_url = Rails.application.routes.default_url_options[:host]
126
+ self.endpoint_namespace = '/api/v1'
127
+
128
+ def current_user
129
+ context.current_user
130
+ end
131
+ end
132
+ ```
133
+
134
+ ### A Resource
135
+
136
+ An individual Resource declares its attributes and relationships, plus anything about it that differs from the defaults:
137
+
138
+ ```ruby
139
+ class EmployeeResource < ApplicationResource
140
+ # Both inferred from the class name. Set them only when they differ
141
+ self.model = Employee
142
+ self.type = :employees # the JSONAPI type
143
+
144
+ self.default_sort = [{ name: :desc }] # default nil
145
+ self.default_page_size = 10 # default 20
146
+
147
+ attribute :name, :string
148
+ attribute :age, :integer
149
+ attribute :hired_at, :datetime, writable: false
150
+
151
+ has_many :positions
152
+ end
153
+ ```
154
+
155
+ That is a complete, working Resource. Everything below is how you override a piece of it.
156
+
157
+ ### Attributes
158
+
159
+ ```ruby
160
+ # Each flag defaults to the corresponding class-level setting
161
+ attribute :name, :string,
162
+ readable: self.attributes_readable_by_default,
163
+ writable: self.attributes_writable_by_default,
164
+ sortable: self.attributes_sortable_by_default,
165
+ filterable: self.attributes_filterable_by_default
166
+
167
+ # Alter display
168
+ # @object is your model instance
169
+ attribute :name, :string do
170
+ @object.name.upcase
171
+ end
172
+ ```
173
+
174
+ ### Sorting
175
+
176
+ ```ruby
177
+ # Pass a type - sort :name, :string - if no attribute is defined
178
+ sort :name do |scope, dir|
179
+ scope.order(name: dir)
180
+ end
181
+ ```
182
+
183
+ ### Filtering
184
+
185
+ ```ruby
186
+ # Pass a type - filter :name, :string - if no attribute is defined
187
+ filter :name do
188
+ # All of these operators have not_ equivalents, e.g. not_eq
189
+ # Imagine ".where.not" instead of ".where"
190
+
191
+ eq do |scope, value|
192
+ scope.where("lower(name) IN ?", value.map(&:downcase))
193
+ end
194
+
195
+ eql do |scope, value|
196
+ scope.where(name: value)
197
+ end
198
+
199
+ prefix do |scope, value|
200
+ value.each do |v|
201
+ scope = scope.where('lower(name) LIKE ?', "#{v.downcase}%")
202
+ end
203
+ scope
204
+ end
205
+
206
+ suffix do |scope, value|
207
+ value.each do |v|
208
+ scope = scope.where('lower(name) LIKE ?', "%#{v.downcase}")
209
+ end
210
+ scope
211
+ end
212
+
213
+ match do |scope, value|
214
+ value.each do |v|
215
+ scope = scope.where('lower(name) LIKE ?', "%#{v.downcase}%")
216
+ end
217
+ scope
218
+ end
219
+ end
220
+
221
+ # Comparison operators, for integer, float, datetime, etc
222
+ filter :age, :integer do
223
+ eq do |scope, value|
224
+ scope.where(age: value)
225
+ end
226
+
227
+ gt do |scope, value|
228
+ value.each { |v| scope = scope.where('age > ?', v) }
229
+ scope
230
+ end
231
+
232
+ gte do |scope, value|
233
+ value.each { |v| scope = scope.where('age >= ?', v) }
234
+ scope
235
+ end
236
+
237
+ lt do |scope, value|
238
+ value.each { |v| scope = scope.where('age < ?', v) }
239
+ scope
240
+ end
241
+
242
+ lte do |scope, value|
243
+ value.each { |v| scope = scope.where('age <= ?', v) }
244
+ scope
245
+ end
246
+ end
247
+ ```
248
+
249
+ Filters receive an array of values by default, which is why each operator above iterates. Pass `single: true` to accept one value instead.
250
+
251
+ ### Querying
252
+
253
+ ```ruby
254
+ # Passed to sort, filter, paginate, etc
255
+ # Apply global logic here: only return active Employees,
256
+ # scope results to the current user, and so on
257
+ def base_scope
258
+ Employee.all
259
+ end
260
+
261
+ # Must execute the query and return an array of Model instances
262
+ def resolve(scope)
263
+ scope.to_a
264
+ end
265
+ ```
266
+
267
+ ### Persisting
268
+
269
+ Your adapter handles writes for you, so most Resources define nothing here. Reach for [lifecycle hooks](/concepts/persisting#persistence-lifecycle-hooks) when you need to intervene:
270
+
271
+ ```ruby
272
+ before_attributes do |attributes|
273
+ # before attributes are assigned to the model
274
+ end
275
+
276
+ before_save do |model|
277
+ # assigned, but not yet persisted
278
+ end
279
+
280
+ before_commit do |model|
281
+ # saved and validated, still inside the transaction
282
+ end
283
+ ```
284
+
285
+ The model you inspect is the model that saves. Attributes are assigned up front, so you can hold the model, check it, and change it before anything is written. The instance you were handed is the one that gets persisted:
286
+
287
+ ```ruby
288
+ employee = EmployeeResource.build(payload)
289
+
290
+ employee.data # the model, attributes already assigned, nothing written yet
291
+ employee.data.valid? # inspect it, or modify it
292
+ employee.save # persists that same instance
293
+ ```
294
+
295
+ Updates work the same way, reading the persisted record until you apply the payload:
296
+
297
+ ```ruby
298
+ proxy = EmployeeResource.find(payload)
299
+ proxy.data.first_name # => "asdf", straight from the database
300
+ proxy.assign_attributes(payload)
301
+ proxy.data.first_name # => "Jane", assigned but still unsaved
302
+ proxy.save(action: :update)
303
+ ```
304
+
305
+ ## Upgrading from 1.x
306
+
307
+ The [2.0 upgrade guide](/upgrading) covers the whole migration: the three gems that folded into core, the deprecated spellings that still work but warn, and the two real behavior changes. Controllers now opt in via `Graphiti::Rails::Controller`, and `around_persistence` receives the model rather than an attributes hash.
@@ -0,0 +1,63 @@
1
+ ---
2
+ title: 'Authentication'
3
+ sidebar_position: 7
4
+ ---
5
+
6
+ ### Authentication
7
+
8
+ Spraypaint supports [JSON Web Tokens](https://jwt.io/introduction). These can
9
+ be set manually, or automatically fetched from `localStorage`.
10
+
11
+ To set manually:
12
+
13
+ ```typescript
14
+ ApplicationRecord.jwt = 'myt0k3n'
15
+ ```
16
+ > All requests will now send the header:<br />
17
+ > `Authorization: Token token="myt0k3n"`.
18
+
19
+ To set via `localStorage`, store the token with a key of `jwt` and it will be set automatically. To customize the `localStorage` key:
20
+
21
+ ```typescript
22
+ ApplicationRecord.jwtStorage = "authtoken"
23
+ ```
24
+
25
+ ...or to opt-out of `localStorage` altogether:
26
+
27
+ ```typescript
28
+ ApplicationRecord.jwtStorage = false
29
+ ```
30
+
31
+ You can control the format of the header that is sent to the
32
+ server:
33
+
34
+ ```typescript
35
+ class ApplicationRecord extends SpraypaintBase {
36
+ // ... code ...
37
+ static generateAuthHeader(token) {
38
+ return `Bearer ${token}`
39
+ }
40
+ }
41
+ ```
42
+
43
+ ```javascript
44
+ var ApplicationRecord = SpraypaintBase.extend({
45
+ // ... code ...
46
+ static: {
47
+ generateAuthHeader: function(token) {
48
+ return "Bearer " + token;
49
+ }
50
+ }
51
+ });
52
+ ```
53
+
54
+ Finally, if your server returns a refreshed JWT within the `X-JWT` header, it will be used in all subsequent requests (and `localStorage`
55
+ will be updated automatically if you're using it).
56
+
57
+ <h2 id="next">
58
+ <a href="/js/state-syncing">
59
+ NEXT:
60
+ <small>State Syncing</small>
61
+ &raquo;
62
+ </a>
63
+ </h2>
data/docs/js/ddau.md ADDED
@@ -0,0 +1,20 @@
1
+ ---
2
+ title: 'Ddau'
3
+ sidebar_position: 9
4
+ ---
5
+
6
+ ### Data Down, Actions Up
7
+
8
+ It's a [popular pattern](http://www.samselikoff.com/blog/data-down-actions-up) to pass data **down** to components, avoid modifying state within the component, and instead pass **actions up** to modify state. This can make complex applications easier to track and reason about, and you'll see it in client-side frameworks like React.
9
+
10
+ To follow this pattern, use `#dup()` when passing down to your component:
11
+
12
+ ```bash
13
+ <my-component something="model.dup()" />
14
+ ```
15
+
16
+ This will create a new instance of the model with all the same state.
17
+ Avoid modifying this instance in your component and instead pass
18
+ **actions up**.
19
+
20
+ When opting-in to [state-syncing](/js/state-syncing) these instances will sync-up whenever one of these is instances is persisted. You won't have to worry about updating the child component when the parent instance is saved.
@@ -0,0 +1,41 @@
1
+ ---
2
+ title: 'Extra Params'
3
+ sidebar_position: 10
4
+ ---
5
+
6
+ ### Extra Params
7
+
8
+ Sometimes you need to submit params that are not standard jsonapi params. One great example would be
9
+ `https://yourdomain.com/users?debug=true` which is not a param for the `UserResource` you may have, but
10
+ might enable functionality in your controller as needed.
11
+
12
+ Invoking it is pretty straightforward, just invoke `extraParams` and pass in params and values you wish
13
+ to add to your API call when executed.
14
+
15
+
16
+ ```typescript
17
+ YourRecord.extraParams({ debug: true })
18
+ ```
19
+
20
+ One common way to use this globally is to put this into a base class so it can be chained as part of
21
+ every resource.
22
+
23
+ ```typescript
24
+ @Model
25
+ export class ApplicationRecord extends SpraypaintBase {
26
+ static withDebug<T extends ApplicationRecord>(): Scope<T> {
27
+ return this.extraParams({ debug: true }) as Scope<T>;
28
+ }
29
+ }
30
+ // unfortunately you will need to pass in the
31
+ // implementing class' type as a generic
32
+ UserRecord.withDebug<UserRecord>().all()
33
+ ```
34
+ ```javascript
35
+ const ApplicationRecord = SpraypaintBase.extend({
36
+ static: {
37
+ withDebug: () => this.extraParams({ debug: true });
38
+ }
39
+ })
40
+ UserRecord.withDebug().all()
41
+ ```
data/docs/js/index.md ADDED
@@ -0,0 +1,112 @@
1
+ ---
2
+ title: 'Index'
3
+ sidebar_position: 1
4
+ ---
5
+
6
+ <h1>
7
+ Spraypaint
8
+ <small>the isomorphic, framework-agnostic Graphiti ORM</small>
9
+ </h1>
10
+
11
+ ### Why Spraypaint?
12
+
13
+ Contracts like JSONAPI and GraphQL treat the API like a database. When querying a database, we have two options:
14
+
15
+ * Type the low-level query language directly (in the database world, this would be hand-typing SQL).
16
+ * Use an ORM (like Rails's `ActiveRecord`, Phoenix's `Ecto`, Django's `DjangoORM`, or Node's `Sequelize`).
17
+
18
+ While both options have pros and cons, we tend to think ORMs have two overwhelming benefits: ***ease of use*** and ***composable queries***. We'll explore both these concepts in other sections.
19
+
20
+ So, we want a javascript ORM for our JSONAPI "database". Because `ActiveRecord` is arguably the most well-known ORM, we've tried to match its interface to make this library accessible to new users. That said, you'll find we've tried to favor *explicitness* over *implicitness* in order to avoid common `ActiveRecord` pitfalls.
21
+
22
+ <span>Typescript</span>
23
+ <span>Javascript</span>
24
+ ```typescript
25
+ // Spraypaint is like "ActiveRecord in Javascript". It can:
26
+ //
27
+ // * Deeply nest reads and writes
28
+ // * Automatically handle validation errors
29
+ // * Replace *ux patterns
30
+ // * ...and much more!
31
+
32
+ // define models
33
+ @Model()
34
+ class ApplicationRecord extends SpraypaintBase {
35
+ static baseUrl = "http://my-api.com"
36
+ static apiNamespace = "/api/v1"
37
+ }
38
+
39
+ @Model()
40
+ class Person extends ApplicationRecord {
41
+ static jsonapiType = "people"
42
+
43
+ @Attr() firstName: string
44
+ @Attr() lastName: string
45
+
46
+ get fullName() {
47
+ return `${this.firstName} ${this.lastName}`
48
+ }
49
+ }
50
+
51
+ // execute queries
52
+ Person
53
+ .where({ first_name: 'John' })
54
+ .order({ created_at: 'desc' })
55
+ .per(10).page(2)
56
+ .includes({ jobs: 'company' })
57
+ .select({ people: ['first_name', 'last_name'] })
58
+
59
+ // persist data
60
+ let person = new Person({ firstName: 'Jane' })
61
+ person.save()
62
+ ```
63
+
64
+ ```javascript
65
+ // Spraypaint is like "ActiveRecord in Javascript". It can:
66
+ //
67
+ // * Deeply nest reads and writes
68
+ // * Automatically handle validation errors
69
+ // * Replace *ux patterns
70
+ // * ...and much more!
71
+
72
+ var spnt = require('spraypaint/dist/spraypaint')
73
+
74
+ // define models
75
+ const ApplicationRecord = spnt.JSORMBase.extend({
76
+ static: {
77
+ baseUrl: 'http://my-api.com',
78
+ apiNamespace: '/api/v1'
79
+ }
80
+ })
81
+
82
+ const Person = ApplicationRecord.extend({
83
+ attrs: {
84
+ firstName: spnt.attr(),
85
+ lastName: spnt.attr()
86
+ },
87
+ methods: {
88
+ fullName: function() {
89
+ return this.firstName + ' ' + this.lastName;
90
+ }
91
+ }
92
+ })
93
+
94
+ // execute queries
95
+ Person
96
+ .where({ first_name: 'John' })
97
+ .order({ created_at: 'desc' })
98
+ .per(10).page(2)
99
+ .includes({ jobs: 'company' })
100
+ .select({ people: ['first_name', 'last_name'] })
101
+
102
+ // persist data
103
+ var person = new Person({ firstName: 'Jane' })
104
+ person.save()
105
+ ```
106
+
107
+ ### Where to Go Next
108
+
109
+ * [Installation](/js/installation) - install spraypaint and connect it to your API
110
+ * [Models](/js/models) - define models, attributes, and relationships
111
+ * [Reads](/js/reads) - query your API with a composable, ActiveRecord-like interface
112
+ * [Writes](/js/writes) - create, update, and destroy records
@@ -0,0 +1,120 @@
1
+ ---
2
+ title: 'Installation'
3
+ sidebar_position: 2
4
+ ---
5
+
6
+ ### Installation
7
+
8
+ Installation is straightforward. Since we use `fetch` underneath the hood, we recommend installing alongside a `fetch` polyfill.
9
+
10
+ If using `yarn`:
11
+
12
+ ```bash
13
+ $ yarn add spraypaint isomorphic-fetch
14
+ ```
15
+
16
+ If using `npm`:
17
+
18
+ ```bash
19
+ $ npm install spraypaint isomorphic-fetch
20
+ ```
21
+
22
+ Now import it:
23
+
24
+ ```typescript
25
+ import {
26
+ Model,
27
+ SpraypaintBase,
28
+ Attr,
29
+ BelongsTo,
30
+ HasMany
31
+ // etc
32
+ } from "spraypaint"
33
+ ```
34
+
35
+ ```javascript
36
+ const {
37
+ SpraypaintBase,
38
+ attr,
39
+ belongsTo,
40
+ hasMany
41
+ // etc
42
+ } = require("spraypaint/dist/spraypaint")
43
+ ```
44
+
45
+ ...or, if you're avoiding JS modules, `spraypaint` will be available as a global in the browser.
46
+
47
+ ### Typescript
48
+
49
+ Spraypaint works with modern TypeScript. Depending on your `tsconfig.json` settings, you may need a `!` after each attribute and relationship declaration:
50
+
51
+ ```typescript
52
+ @Attr first_name!: string
53
+ @HasMany() positions!: Position[]
54
+ ```
55
+
56
+ This is because of [Strict Class Initialization](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html#strict-class-initialization) - `strictPropertyInitialization` expects every declared class field to be assigned in the constructor, which Spraypaint's decorators handle at runtime rather than at construction time. For the purposes of Spraypaint, we don't need this check. Remove the need for `!` (as the rest of these guides do) by setting
57
+
58
+ `"strictPropertyInitialization": false`
59
+
60
+ in `tsconfig.json`.
61
+
62
+ ### Connecting to the API
63
+
64
+ Just like `ActiveRecord`, our models will inherit from a base class that holds connection information (`ApplicationRecord`, or `ActiveRecord::Base` in Rails < 5):
65
+
66
+ ```typescript
67
+ @Model()
68
+ class ApplicationRecord extends SpraypaintBase {
69
+ static baseUrl = "http://my-api.com"
70
+ static apiNamespace = "/api/v1"
71
+ }
72
+ ```
73
+
74
+ ```javascript
75
+ const ApplicationRecord = SpraypaintBase.extend({
76
+ static: {
77
+ baseUrl: "http://my-api.com",
78
+ apiNamespace: "/api/v1"
79
+ }
80
+ })
81
+ ```
82
+
83
+ All URLs follow the following pattern:
84
+
85
+ * `baseUrl` + `apiNamespace` + `jsonapiType`
86
+
87
+ As you can see above, typically `baseUrl` and `apiNamespace` are set on a top-level `ApplicationRecord` (though any subclass can override). `jsonapiType`, however, is set per-model - see [Models](/js/models) for how to define it.
88
+
89
+ > **TIP**: Avoid CORS and use relative paths by setting `baseUrl` to `""`
90
+
91
+ > **TIP**: You can always use the `endpoint` option to override this pattern and set the endpoint manually.
92
+
93
+ #### Setting Application Name
94
+
95
+ It can be helpful to send the name of your client application in request headers. With this information, servers can keep track of which clients are hitting which APIs.
96
+
97
+ To do this:
98
+
99
+ ```typescript
100
+ @Model()
101
+ class Person extends ApplicationRecord {
102
+ static clientApplication = "sales-backend"
103
+ }
104
+ ```
105
+
106
+ ```javascript
107
+ const Person = ApplicationRecord.extend({
108
+ static: {
109
+ clientApplication: "sales-backend"
110
+ }
111
+ })
112
+ ```
113
+
114
+ <h2 id="next">
115
+ <a href="/js/models">
116
+ NEXT:
117
+ <small>Models</small>
118
+ &raquo;
119
+ </a>
120
+ </h2>