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,73 @@
1
+ module Graphiti
2
+ module SpecHelpers
3
+ module Errors
4
+ class Base < StandardError; end
5
+
6
+ class LinksNotFound < Base
7
+ def initialize(name)
8
+ @name = name
9
+ end
10
+
11
+ def message
12
+ "Relationship with name '#{@name}' has no links!"
13
+ end
14
+ end
15
+
16
+ class NoAttribute < Base
17
+ def initialize(name)
18
+ @name = name
19
+ end
20
+
21
+ def message
22
+ "No attribute '#{@name}' in JSON response node!"
23
+ end
24
+ end
25
+
26
+ class SideloadNotFound < Base
27
+ def initialize(name)
28
+ @name = name
29
+ end
30
+
31
+ def message
32
+ "Relationship with name '#{@name}' not found!"
33
+ end
34
+ end
35
+
36
+ class NoResponse < Base
37
+ def message
38
+ "Cannot parse response - missing #response.body!"
39
+ end
40
+ end
41
+
42
+ class NoData < Base
43
+ def initialize(payload)
44
+ @payload = payload
45
+ end
46
+
47
+ def message
48
+ "Payload did not contain 'data'! Payload was:\n\n#{JSON.pretty_generate(@payload)}"
49
+ end
50
+ end
51
+
52
+ class NoSideloads < Base
53
+ def initialize(payload)
54
+ @payload = payload
55
+ end
56
+
57
+ def message
58
+ "Tried to find sideload, but the payload did not contain 'included'! Payload was:\n\n#{JSON.pretty_generate(@payload)}"
59
+ end
60
+ end
61
+
62
+ class NoMeta < Base
63
+ def initialize(payload)
64
+ @payload = payload
65
+ end
66
+
67
+ def message
68
+ "Tried to find metadata, but the payload did not contain 'meta'! Payload was:\n\n#{JSON.pretty_generate(@payload)}"
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,75 @@
1
+ module Graphiti
2
+ module SpecHelpers
3
+ class ErrorsProxy
4
+ class Error
5
+ attr_reader :json
6
+
7
+ def initialize(json)
8
+ @json = json
9
+ end
10
+
11
+ def attribute
12
+ @json[:meta][:attribute]
13
+ end
14
+
15
+ # TODO: move to top-level code in errorable
16
+ def code
17
+ @json[:meta][:code]
18
+ end
19
+
20
+ def message
21
+ @json[:meta][:message]
22
+ end
23
+
24
+ def title
25
+ @json[:title]
26
+ end
27
+
28
+ def detail
29
+ @json[:detail]
30
+ end
31
+
32
+ def status
33
+ @json[:status]
34
+ end
35
+ end
36
+
37
+ include Enumerable
38
+
39
+ def initialize(array)
40
+ @errors = array.map { |e| Error.new(e) }
41
+ end
42
+
43
+ def [](key)
44
+ @errors[key]
45
+ end
46
+
47
+ def each(&blk)
48
+ @errors.each(&blk)
49
+ end
50
+
51
+ def length
52
+ count
53
+ end
54
+
55
+ def to_h
56
+ {}.tap do |hash|
57
+ @errors.each do |e|
58
+ hash[e.attribute] = e.message
59
+ end
60
+ end
61
+ end
62
+
63
+ def method_missing(id, *args, &blk)
64
+ matches = @errors.select { |e| e.attribute.to_sym == id }
65
+ matches.length == 1 ? matches[0] : matches
66
+ end
67
+
68
+ # Any name is answered: an attribute with no matching error yields an empty
69
+ # array rather than raising NoMethodError.
70
+ def respond_to_missing?(id, include_private = false)
71
+ true
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,107 @@
1
+ module Graphiti
2
+ module SpecHelpers
3
+ module Helpers
4
+ extend ActiveSupport::Concern
5
+
6
+ def json
7
+ if response&.body
8
+ JSON.parse(response.body).with_indifferent_access
9
+ else
10
+ raise Errors::NoResponse.new
11
+ end
12
+ end
13
+
14
+ def jsonapi_data
15
+ @jsonapi_data ||= if _jsonapi_data.is_a?(Hash)
16
+ node(from: _jsonapi_data)
17
+ else
18
+ _jsonapi_data.map { |datum| node(from: datum) }
19
+ end
20
+ end
21
+
22
+ def jsonapi_included(type = nil)
23
+ variable = :"@jsonapi_included#{type}"
24
+ memo = instance_variable_get(variable)
25
+ return memo if memo
26
+
27
+ nodes = _jsonapi_included.map { |i| node(from: i) }
28
+ if type
29
+ nodes.select! { |n| n.jsonapi_type == type.to_s }
30
+ end
31
+ instance_variable_set(variable, nodes)
32
+ nodes
33
+ end
34
+
35
+ def jsonapi_errors
36
+ @jsonapi_errors ||= ErrorsProxy.new(json["errors"] || [])
37
+ end
38
+
39
+ def jsonapi_meta
40
+ @jsonapi_errors = json["meta"] || raise(Errors::NoMeta.new(json))
41
+ end
42
+
43
+ def jsonapi_headers
44
+ media_type = "application/vnd.api+json"
45
+ {
46
+ "CONTENT_TYPE" => media_type,
47
+ "HTTP_ACCEPT" => media_type
48
+ }
49
+ end
50
+
51
+ def jsonapi_get(url, params: {}, headers: {})
52
+ get url_for(url), params: params, headers: jsonapi_headers.merge(headers)
53
+ end
54
+
55
+ def jsonapi_post(url, payload, headers: {})
56
+ post url_for(url), params: payload.to_json, headers: jsonapi_headers.merge(headers)
57
+ end
58
+
59
+ def jsonapi_put(url, payload, headers: {})
60
+ put url_for(url), params: payload.to_json, headers: jsonapi_headers.merge(headers)
61
+ end
62
+
63
+ def jsonapi_patch(url, payload, headers: {})
64
+ patch url_for(url), params: payload.to_json, headers: jsonapi_headers.merge(headers)
65
+ end
66
+
67
+ def jsonapi_delete(url, headers: {})
68
+ delete url_for(url), headers: jsonapi_headers.merge(headers)
69
+ end
70
+
71
+ def json_datetime(value)
72
+ Graphiti::Types[:datetime][:read][value].as_json
73
+ end
74
+
75
+ def json_date(value)
76
+ Graphiti::Types[:date][:read][value].as_json
77
+ end
78
+
79
+ # @api private
80
+ def node(from: nil)
81
+ from = json if from.nil?
82
+ data = from.has_key?("data") ? from["data"] : from
83
+ hash = {}
84
+ hash["id"] = data["id"]
85
+ hash["jsonapi_type"] = data["type"]
86
+ hash.merge!(data["attributes"]) if data.has_key?("attributes")
87
+ Node.new(hash, data["relationships"], self)
88
+ end
89
+
90
+ private
91
+
92
+ # @api private
93
+ def _jsonapi_data
94
+ json["data"] || raise(Errors::NoData.new(json))
95
+ end
96
+
97
+ # @api private
98
+ def _jsonapi_included
99
+ if json.has_key?("included")
100
+ json["included"]
101
+ else
102
+ raise Errors::NoSideloads.new(json)
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,88 @@
1
+ module Graphiti
2
+ module SpecHelpers
3
+ class Node
4
+ attr_reader :attributes, :relationships
5
+
6
+ def initialize(attributes, relationships, context)
7
+ @attributes = attributes.with_indifferent_access
8
+ @relationships = relationships.with_indifferent_access if relationships
9
+ @context = context
10
+ end
11
+
12
+ def id
13
+ Integer(rawid) # Only convert if using integer ids
14
+ rescue ArgumentError
15
+ rawid
16
+ end
17
+
18
+ def rawid
19
+ @attributes["id"]
20
+ end
21
+
22
+ def jsonapi_type
23
+ @attributes["jsonapi_type"]
24
+ end
25
+
26
+ def has_key?(key)
27
+ @attributes.has_key?(key)
28
+ end
29
+ alias_method :key?, :has_key?
30
+
31
+ def [](key)
32
+ @attributes[key] || @attributes[key.to_s]
33
+ end
34
+
35
+ def []=(key, val)
36
+ @attributes[key] = val
37
+ end
38
+
39
+ alias_method :to_hash, :attributes
40
+
41
+ def method_missing(id, *args, &blk)
42
+ if @attributes.has_key?(id)
43
+ @attributes[id]
44
+ else
45
+ raise Errors::NoAttribute.new(id)
46
+ end
47
+ end
48
+
49
+ def respond_to_missing?(id, include_private = false)
50
+ @attributes.has_key?(id) || super
51
+ end
52
+
53
+ def link(relationship_name, name)
54
+ if @relationships.has_key?(relationship_name)
55
+ links = @relationships[relationship_name][:links]
56
+ raise Errors::LinksNotFound.new(relationship_name) unless links
57
+ links[name]
58
+ else
59
+ raise Errors::SideloadNotFound.new(relationship_name)
60
+ end
61
+ end
62
+
63
+ def sideload(relationship_name)
64
+ unless @relationships.has_key?(relationship_name)
65
+ raise Errors::SideloadNotFound.new(relationship_name)
66
+ end
67
+ rel = @relationships[relationship_name]
68
+ rel = rel[:data]
69
+ return if rel.nil?
70
+ if rel.is_a?(Hash)
71
+ include_for(rel[:type], rel[:id])
72
+ else
73
+ rel.map { |r| include_for(r[:type], r[:id]) }
74
+ end
75
+ end
76
+ alias_method :sideloads, :sideload
77
+
78
+ private
79
+
80
+ def include_for(type, id)
81
+ data = @context.json["included"].find do |i|
82
+ i["type"] == type && i["id"] == id
83
+ end
84
+ @context.node(from: data)
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,147 @@
1
+ require "rspec/core"
2
+ require "graphiti/spec_helpers"
3
+
4
+ module Graphiti
5
+ module SpecHelpers
6
+ class ContextProxy < OpenStruct
7
+ def initialize(proxied, *args)
8
+ @__proxied = proxied
9
+ super(*args)
10
+ end
11
+
12
+ # variables defined in the rspec context should remain lazy
13
+ def current_user
14
+ super || __proxied_current_user
15
+ end
16
+
17
+ def params
18
+ super || __proxied_params
19
+ end
20
+
21
+ private
22
+
23
+ def __proxied_current_user
24
+ @__proxied.current_user if @__proxied.respond_to?(:current_user)
25
+ end
26
+
27
+ def __proxied_params
28
+ @__proxied.params if @__proxied.respond_to?(:params)
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ # Registered under both names. The graphiti-prefixed name is canonical. The bare
35
+ # one is kept for suites that include it explicitly, and carries no `type:`
36
+ # metadata so a `type: :resource` group does not pick the context up twice.
37
+ resource_testing = proc do
38
+ let(:resource) { described_class }
39
+ let(:params) { {} }
40
+
41
+ around do |e|
42
+ original = Graphiti::Resource.validate_endpoints
43
+ Graphiti::Resource.validate_endpoints = false
44
+
45
+ Graphiti.with_context graphiti_context do
46
+ e.run
47
+ end
48
+ ensure
49
+ Graphiti::Resource.validate_endpoints = original
50
+ end
51
+
52
+ def graphiti_context
53
+ @graphiti_context ||= Graphiti::SpecHelpers::ContextProxy.new(self)
54
+ end
55
+
56
+ # If you need to set context:
57
+ #
58
+ # Graphiti.with_context my_context, {} do
59
+ # render
60
+ # end
61
+ def render(runtime_options = {})
62
+ json = proxy.to_jsonapi(runtime_options)
63
+ response.body = json
64
+ json
65
+ end
66
+
67
+ def proxy
68
+ @proxy ||= begin
69
+ args = [params]
70
+ args << base_scope if defined?(base_scope)
71
+ resource.all(*args)
72
+ end
73
+ end
74
+
75
+ def records
76
+ proxy.data
77
+ end
78
+
79
+ def response
80
+ @response ||= OpenStruct.new
81
+ end
82
+ end
83
+
84
+ remote_api = proc do
85
+ # Fake request headers
86
+ around do |e|
87
+ ctx = OpenStruct.new \
88
+ request: OpenStruct.new(headers: OpenStruct.new)
89
+ Graphiti.with_context(ctx) { e.run }
90
+ end
91
+
92
+ def mock_api(url, json, call_count = 1)
93
+ api_response = double(body: json.to_json)
94
+ expect(Faraday).to receive(:get)
95
+ .with(url, anything, anything)
96
+ .exactly(call_count).times
97
+ .and_return(api_response)
98
+ end
99
+ end
100
+
101
+ # Was a top-level constant, and reopening it to add context methods was a way
102
+ # suites customised graphiti_context. Remove in 3.0.
103
+ GraphitiContextProxy = ActiveSupport::Deprecation::DeprecatedConstantProxy.new(
104
+ "GraphitiContextProxy",
105
+ "Graphiti::SpecHelpers::ContextProxy",
106
+ Graphiti::DEPRECATOR
107
+ )
108
+
109
+ ::RSpec.shared_context("graphiti resource testing", type: :resource, &resource_testing)
110
+ ::RSpec.shared_context("resource testing", &resource_testing)
111
+
112
+ ::RSpec.shared_context("graphiti remote api", &remote_api)
113
+ ::RSpec.shared_context("remote api", &remote_api)
114
+
115
+ module Graphiti
116
+ module SpecHelpers
117
+ module RSpec
118
+ def self.included(klass)
119
+ klass.send(:include, Graphiti::SpecHelpers)
120
+
121
+ ::RSpec.configure do |rspec|
122
+ rspec.include_context "graphiti resource testing", type: :resource
123
+ end
124
+ end
125
+
126
+ def self.schema!(resources = nil)
127
+ ::RSpec.describe "Graphiti Schema" do
128
+ it "generates a backwards-compatible schema" do
129
+ message = <<~MSG
130
+ Found backwards-incompatibilities in schema! Run with FORCE_SCHEMA=true to ignore.
131
+
132
+ Incompatibilities:
133
+
134
+ MSG
135
+
136
+ errors = Graphiti::Schema.generate!(resources)
137
+ errors.each do |e|
138
+ message << "#{e}\n"
139
+ end
140
+
141
+ expect(errors.empty?).to eq(true), message
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,53 @@
1
+ # Merged in from the standalone graphiti_spec_helpers gem, retired as of
2
+ # Graphiti 2.0. The GraphitiSpecHelpers namespace and the
3
+ # "graphiti_spec_helpers/rspec" require path are kept verbatim so existing spec
4
+ # suites need no changes beyond dropping the gem from their Gemfile.
5
+ require "json"
6
+ require "active_support/core_ext/string"
7
+ require "active_support/core_ext/hash"
8
+ require "graphiti"
9
+
10
+ require "graphiti/spec_helpers/helpers"
11
+ require "graphiti/spec_helpers/node"
12
+ require "graphiti/spec_helpers/errors_proxy"
13
+ require "graphiti/spec_helpers/errors"
14
+
15
+ module Graphiti
16
+ module SpecHelpers
17
+ def self.included(klass)
18
+ klass.send(:include, Helpers)
19
+ end
20
+
21
+ class TestRunner < ::Graphiti::Runner
22
+ def current_user
23
+ nil
24
+ end
25
+ end
26
+
27
+ module Sugar
28
+ def d
29
+ jsonapi_data
30
+ end
31
+
32
+ def included(type = nil)
33
+ jsonapi_included(type)
34
+ end
35
+
36
+ def errors
37
+ jsonapi_errors
38
+ end
39
+
40
+ def dt(*args)
41
+ json_datetime(*args)
42
+ end
43
+
44
+ def datetime(*args)
45
+ json_datetime(*args)
46
+ end
47
+
48
+ def date(*args)
49
+ json_date(*args)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -7,9 +7,9 @@ module Graphiti
7
7
  #
8
8
  # GET /posts?include=comments.author
9
9
  #
10
- # But our resource had this code:
10
+ # But the controller had this code:
11
11
  #
12
- # sideload_allowlist({ index: [:comments] })
12
+ # self.sideload_allowlist = {index: [:comments]}
13
13
  #
14
14
  # We should drop the 'author' sideload from the request.
15
15
  #
@@ -190,22 +190,21 @@ class Graphiti::Util::Persistence
190
190
  }
191
191
  end
192
192
 
193
- def accepts_assigned_model?(method)
194
- method.parameters.any? { |type, name| name == :assigned_model && [:key, :keyreq].include?(type) }
195
- end
196
-
193
+ # The assigned model rides on the resource instance rather than through
194
+ # the method signature, so any override of #create/#update - whatever its
195
+ # arity - passes it through untouched.
197
196
  def call_resource_method(method_name, attributes, caller_model)
198
197
  method = @resource.method(method_name)
198
+ call = if method.arity == 1
199
+ -> { method.call(attributes) }
200
+ else
201
+ -> { method.call(attributes, metadata) }
202
+ end
199
203
 
200
204
  if @assigned_model && [:create, :update].include?(method_name)
201
- unless accepts_assigned_model?(method)
202
- raise Graphiti::Errors::AssignedModelNotSupported.new(@resource.class, method_name)
203
- end
204
- method.call(attributes, metadata, assigned_model: @assigned_model)
205
- elsif method.arity == 1
206
- method.call(attributes)
205
+ @resource.with_assigned_model(@assigned_model) { call.call }
207
206
  else
208
- method.call(attributes, metadata)
207
+ call.call
209
208
  end
210
209
  end
211
210
  end
@@ -19,7 +19,14 @@ module Graphiti
19
19
  private
20
20
 
21
21
  def apply?(sideload)
22
- @serializer.relationship_blocks[sideload.name].nil?
22
+ return true if @serializer.relationship_blocks[sideload.name].nil?
23
+
24
+ # A subclass inherits its parent's relationship blocks, each closed
25
+ # over the parent's sideload. Redeclaring the relationship has to
26
+ # replace that block or the override never reaches the payload.
27
+ # Anything not generated here was written by hand, so leave it.
28
+ applied = @serializer.relationship_sideloads[sideload.name]
29
+ !applied.nil? && !applied.equal?(sideload)
23
30
  end
24
31
  end
25
32
 
@@ -32,6 +39,10 @@ module Graphiti
32
39
 
33
40
  def apply
34
41
  sideload = @sideload
42
+ # Reassign rather than mutate: ancestors share the hash by reference
43
+ # until a subclass writes to it.
44
+ @serializer.relationship_sideloads =
45
+ @serializer.relationship_sideloads.merge(@sideload.name => @sideload)
35
46
  @serializer.relationship(@sideload.name, if: -> { sideload.readable? }, &block)
36
47
  end
37
48
 
@@ -53,9 +64,25 @@ module Graphiti
53
64
  proc do
54
65
  data { instance_eval(&data_proc_ref) }
55
66
 
56
- # include relationship links for belongs_to relationships
57
- # https://github.com/graphiti-api/graphiti/issues/167
58
- linkage always: sideload_ref.always_include_resource_ids?
67
+ # An included relationship is already loaded, and a customized
68
+ # sideload can resolve it to something the foreign key alone would
69
+ # not predict, so the loaded records win. Only the un-included case
70
+ # is worth short-circuiting.
71
+ if sideload_ref.linkage_from_foreign_key? &&
72
+ !self_ref.send(:included_anywhere?, @proxy.query.include_hash, sideload_ref.name)
73
+ linkage always: sideload_ref.always_include_resource_ids? do
74
+ foreign_key = @object.public_send(sideload_ref.foreign_key)
75
+
76
+ unless foreign_key.nil?
77
+ {
78
+ type: sideload_ref.resource.type,
79
+ id: foreign_key.to_s
80
+ }
81
+ end
82
+ end
83
+ else
84
+ linkage always: sideload_ref.always_include_resource_ids?
85
+ end
59
86
 
60
87
  if link_ref
61
88
  if @proxy.query.links?
@@ -69,6 +96,15 @@ module Graphiti
69
96
  end
70
97
  end
71
98
 
99
+ # A relationship nested under another one is still loaded and can still
100
+ # be narrowed by a deep filter, so the foreign key is not a safe
101
+ # stand-in for what the request actually returns.
102
+ def included_anywhere?(include_hash, name)
103
+ include_hash.any? do |key, nested|
104
+ key == name || included_anywhere?(nested, name)
105
+ end
106
+ end
107
+
72
108
  def data_proc
73
109
  sideload_ref = @sideload
74
110
  ->(_) {
@@ -85,7 +121,7 @@ module Graphiti
85
121
  end
86
122
 
87
123
  def eagerly_validate_links?
88
- # TODO: Maybe handle this in graphiti-rails
124
+ # TODO: Maybe handle this in the Rails integration
89
125
  if defined?(::Rails) && (app = ::Rails.application)
90
126
  app.config.eager_load
91
127
  else
@@ -1,3 +1,3 @@
1
1
  module Graphiti
2
- VERSION = "2.0.0.beta.2"
2
+ VERSION = "2.0.0.beta.4"
3
3
  end
@@ -0,0 +1,11 @@
1
+ # Deprecated entry point. The graphiti-rails gem was absorbed into graphiti in
2
+ # 2.0, and its integration loads automatically whenever Rails is defined, so
3
+ # this require is no longer needed. It is kept so an explicit
4
+ # `require "graphiti-rails"` left over from 1.x does not raise. Remove in 3.0.
5
+ require "graphiti"
6
+ require "graphiti/rails"
7
+
8
+ Graphiti::DEPRECATOR.warn(
9
+ 'require "graphiti-rails" is no longer needed. graphiti-rails is part of graphiti as of 2.0, ' \
10
+ "and the Rails integration is loaded for you when Rails is present."
11
+ )