graphiti 2.0.0.beta.3 → 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 (267) 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 +34 -0
  10. data/README.md +13 -2
  11. data/UPGRADING.md +2 -58
  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 -4
  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/remote.rb +2 -2
  88. data/lib/graphiti/resource/sideloading.rb +1 -1
  89. data/lib/graphiti/resource.rb +13 -1
  90. data/lib/graphiti/responders.rb +7 -20
  91. data/lib/graphiti/schema.rb +5 -1
  92. data/lib/graphiti/schema_diff.rb +4 -0
  93. data/lib/graphiti/scope.rb +45 -37
  94. data/lib/graphiti/serializer.rb +6 -0
  95. data/lib/graphiti/sideload/belongs_to.rb +38 -5
  96. data/lib/graphiti/sideload/polymorphic_belongs_to.rb +27 -23
  97. data/lib/graphiti/sideload.rb +54 -35
  98. data/lib/graphiti/spec_helpers/errors.rb +73 -0
  99. data/lib/graphiti/spec_helpers/errors_proxy.rb +75 -0
  100. data/lib/graphiti/spec_helpers/helpers.rb +107 -0
  101. data/lib/graphiti/spec_helpers/node.rb +88 -0
  102. data/lib/graphiti/spec_helpers/rspec.rb +147 -0
  103. data/lib/graphiti/spec_helpers.rb +53 -0
  104. data/lib/graphiti/util/include_params.rb +2 -2
  105. data/lib/graphiti/util/serializer_relationships.rb +41 -5
  106. data/lib/graphiti/version.rb +1 -1
  107. data/lib/graphiti-rails.rb +11 -0
  108. data/lib/graphiti.rb +34 -10
  109. data/lib/graphiti_errors.rb +11 -0
  110. data/lib/graphiti_spec_helpers/rspec.rb +3 -0
  111. data/lib/graphiti_spec_helpers.rb +11 -0
  112. data/lib/{graphiti/deprecated_tasks.rb → tasks/graphiti.rake} +6 -1
  113. data/package-lock.json +6199 -0
  114. data/package.json +5 -4
  115. data/website/.gitignore +20 -0
  116. data/website/README.md +43 -0
  117. data/website/docusaurus.config.js +141 -0
  118. data/website/package-lock.json +19474 -0
  119. data/website/package.json +46 -0
  120. data/website/sidebars.js +82 -0
  121. data/website/src/css/custom.css +58 -0
  122. data/website/src/pages/markdown-page.mdx +7 -0
  123. data/website/static/.nojekyll +0 -0
  124. data/website/static/1.13/2019/03/31/graphiti-1-0.html +205 -0
  125. data/website/static/1.13/2019/05/08/graphiti-1-1.html +212 -0
  126. data/website/static/1.13/2019/05/20/graphiti-1-2.html +214 -0
  127. data/website/static/1.13/2019/10/14/tutorial.html +198 -0
  128. data/website/static/1.13/CNAME +1 -0
  129. data/website/static/1.13/README.md +16 -0
  130. data/website/static/1.13/assets/css/syntax.css +60 -0
  131. data/website/static/1.13/assets/favicons/android-chrome-192x192.png +0 -0
  132. data/website/static/1.13/assets/favicons/android-chrome-256x256.png +0 -0
  133. data/website/static/1.13/assets/favicons/apple-touch-icon.png +0 -0
  134. data/website/static/1.13/assets/favicons/browserconfig.xml +9 -0
  135. data/website/static/1.13/assets/favicons/favicon-16x16.png +0 -0
  136. data/website/static/1.13/assets/favicons/favicon-32x32.png +0 -0
  137. data/website/static/1.13/assets/favicons/favicon.ico +0 -0
  138. data/website/static/1.13/assets/favicons/mstile-150x150.png +0 -0
  139. data/website/static/1.13/assets/favicons/safari-pinned-tab.svg +1 -0
  140. data/website/static/1.13/assets/favicons/site.webmanifest +19 -0
  141. data/website/static/1.13/assets/img/backend.gif +0 -0
  142. data/website/static/1.13/assets/img/conformity.png +0 -0
  143. data/website/static/1.13/assets/img/error_payload.png +0 -0
  144. data/website/static/1.13/assets/img/gh.png +0 -0
  145. data/website/static/1.13/assets/img/lifecycle.gif +0 -0
  146. data/website/static/1.13/assets/img/logo-500.png +0 -0
  147. data/website/static/1.13/assets/img/logo.png +0 -0
  148. data/website/static/1.13/assets/img/love-graffiti.jpg +0 -0
  149. data/website/static/1.13/assets/img/meta_total_count.png +0 -0
  150. data/website/static/1.13/assets/img/persist.jpg +0 -0
  151. data/website/static/1.13/assets/img/resource.gif +0 -0
  152. data/website/static/1.13/assets/img/rest-graffiti.jpg +0 -0
  153. data/website/static/1.13/assets/img/rest1.gif +0 -0
  154. data/website/static/1.13/assets/img/rest2.gif +0 -0
  155. data/website/static/1.13/assets/img/rest3.gif +0 -0
  156. data/website/static/1.13/assets/img/rethink-rest-graffiti.jpg +0 -0
  157. data/website/static/1.13/assets/img/why.png +0 -0
  158. data/website/static/1.13/assets/js/highlight.pack.js +2 -0
  159. data/website/static/1.13/assets/main.css +15518 -0
  160. data/website/static/1.13/assets/main.css.map +1 -0
  161. data/website/static/1.13/bin/bundle +109 -0
  162. data/website/static/1.13/bin/jekyll +27 -0
  163. data/website/static/1.13/bin/kramdown +27 -0
  164. data/website/static/1.13/bin/listen +27 -0
  165. data/website/static/1.13/bin/rake +27 -0
  166. data/website/static/1.13/bin/rougify +27 -0
  167. data/website/static/1.13/bin/safe_yaml +27 -0
  168. data/website/static/1.13/bin/sass +27 -0
  169. data/website/static/1.13/bin/sass-convert +27 -0
  170. data/website/static/1.13/bin/scss +27 -0
  171. data/website/static/1.13/blog.html +259 -0
  172. data/website/static/1.13/cheatsheet.html +316 -0
  173. data/website/static/1.13/cookbooks/authorization.md +0 -0
  174. data/website/static/1.13/cookbooks/caching.md +0 -0
  175. data/website/static/1.13/cookbooks/customizing-sideloads.html +325 -0
  176. data/website/static/1.13/cookbooks/etags.md +0 -0
  177. data/website/static/1.13/cookbooks/hopping-relationships.html +324 -0
  178. data/website/static/1.13/cookbooks/json_attributes.md +0 -0
  179. data/website/static/1.13/cookbooks/openstruct-models.md +0 -0
  180. data/website/static/1.13/cookbooks/remote-resources.md +0 -0
  181. data/website/static/1.13/cookbooks/without-activerecord.html +510 -0
  182. data/website/static/1.13/features.html +249 -0
  183. data/website/static/1.13/feed.xml +106 -0
  184. data/website/static/1.13/guides/concepts/backends-and-models.html +467 -0
  185. data/website/static/1.13/guides/concepts/debugging.html +440 -0
  186. data/website/static/1.13/guides/concepts/endpoints.html +432 -0
  187. data/website/static/1.13/guides/concepts/error-handling.html +396 -0
  188. data/website/static/1.13/guides/concepts/links.html +501 -0
  189. data/website/static/1.13/guides/concepts/remote-resources.html +536 -0
  190. data/website/static/1.13/guides/concepts/resources.html +2176 -0
  191. data/website/static/1.13/guides/concepts/testing.html +1469 -0
  192. data/website/static/1.13/guides/getting-started/installation.html +420 -0
  193. data/website/static/1.13/guides/graphiti-rails-migration.html +242 -0
  194. data/website/static/1.13/guides/index.html +269 -0
  195. data/website/static/1.13/guides/overview.html +325 -0
  196. data/website/static/1.13/guides/upgrading-2-0.html +193 -0
  197. data/website/static/1.13/guides/upgrading.html +314 -0
  198. data/website/static/1.13/guides/vandal.html +282 -0
  199. data/website/static/1.13/guides/why.html +1121 -0
  200. data/website/static/1.13/index.html +72 -0
  201. data/website/static/1.13/js/authentication.html +295 -0
  202. data/website/static/1.13/js/ddau.html +238 -0
  203. data/website/static/1.13/js/extra-params.html +270 -0
  204. data/website/static/1.13/js/index.html +321 -0
  205. data/website/static/1.13/js/installation.html +637 -0
  206. data/website/static/1.13/js/introduction.html +257 -0
  207. data/website/static/1.13/js/middleware.html +318 -0
  208. data/website/static/1.13/js/reads/fieldsets.html +271 -0
  209. data/website/static/1.13/js/reads/filtering.html +289 -0
  210. data/website/static/1.13/js/reads/includes.html +260 -0
  211. data/website/static/1.13/js/reads/index.html +497 -0
  212. data/website/static/1.13/js/reads/nested-queries.html +353 -0
  213. data/website/static/1.13/js/reads/pagination.html +260 -0
  214. data/website/static/1.13/js/reads/sorting.html +265 -0
  215. data/website/static/1.13/js/reads/statistics.html +289 -0
  216. data/website/static/1.13/js/state-syncing.html +340 -0
  217. data/website/static/1.13/js/writes/deferred.html +296 -0
  218. data/website/static/1.13/js/writes/dirty-tracking.html +399 -0
  219. data/website/static/1.13/js/writes/index.html +391 -0
  220. data/website/static/1.13/js/writes/nested.html +330 -0
  221. data/website/static/1.13/js/writes/validations.html +272 -0
  222. data/website/static/1.13/quickstart.html +660 -0
  223. data/website/static/1.13/template +161 -0
  224. data/website/static/1.13/tutorial/index.html +250 -0
  225. data/website/static/1.13/tutorial/step_0.html +292 -0
  226. data/website/static/1.13/tutorial/step_1.html +517 -0
  227. data/website/static/1.13/tutorial/step_2.html +481 -0
  228. data/website/static/1.13/tutorial/step_3.html +323 -0
  229. data/website/static/1.13/tutorial/step_4.html +318 -0
  230. data/website/static/1.13/tutorial/step_5.html +265 -0
  231. data/website/static/1.13/tutorial/step_6.html +276 -0
  232. data/website/static/1.13/tutorial/step_7.html +390 -0
  233. data/website/static/1.13/tutorial/step_8.html +316 -0
  234. data/website/static/1.13/tutorial/step_9.html +365 -0
  235. data/website/static/assets/img/error_payload.png +0 -0
  236. data/website/static/assets/img/legacy/legacy-0378a3bb39.png +0 -0
  237. data/website/static/assets/img/legacy/legacy-05bbd3e5fd.png +0 -0
  238. data/website/static/assets/img/legacy/legacy-07aa104495.png +0 -0
  239. data/website/static/assets/img/legacy/legacy-0c75a16b3a.gif +0 -0
  240. data/website/static/assets/img/legacy/legacy-3076df6209.png +0 -0
  241. data/website/static/assets/img/legacy/legacy-7f6889bc89.png +0 -0
  242. data/website/static/assets/img/legacy/legacy-a2cc4363c3.png +0 -0
  243. data/website/static/assets/img/legacy/legacy-f67cfa89ab.png +0 -0
  244. data/website/static/assets/img/meta_total_count.png +0 -0
  245. data/website/static/img/docusaurus-social-card.jpg +0 -0
  246. data/website/static/img/docusaurus.png +0 -0
  247. data/website/static/img/favicon.ico +0 -0
  248. data/website/static/img/logo.png +0 -0
  249. data/website/static/img/logo.svg +1 -0
  250. data/website/static/img/undraw_docusaurus_mountain.svg +171 -0
  251. data/website/static/img/undraw_docusaurus_react.svg +170 -0
  252. data/website/static/img/undraw_docusaurus_tree.svg +40 -0
  253. metadata +245 -46
  254. data/gemfiles/rails_6.gemfile +0 -18
  255. data/gemfiles/rails_6_graphiti_rails.gemfile +0 -19
  256. data/gemfiles/rails_7.gemfile +0 -18
  257. data/gemfiles/rails_7_1_graphiti_rails.gemfile +0 -19
  258. data/gemfiles/rails_7_graphiti_rails.gemfile +0 -19
  259. data/lib/graphiti/railtie.rb +0 -121
  260. /data/{deprecated_generators → lib/generators}/graphiti/resource_test_generator.rb +0 -0
  261. /data/{deprecated_generators → lib/generators}/graphiti/templates/application_resource.rb.erb +0 -0
  262. /data/{deprecated_generators → lib/generators}/graphiti/templates/controller.rb.erb +0 -0
  263. /data/{deprecated_generators → lib/generators}/graphiti/templates/create_request_spec.rb.erb +0 -0
  264. /data/{deprecated_generators → lib/generators}/graphiti/templates/destroy_request_spec.rb.erb +0 -0
  265. /data/{deprecated_generators → lib/generators}/graphiti/templates/resource.rb.erb +0 -0
  266. /data/{deprecated_generators → lib/generators}/graphiti/templates/resource_writes_spec.rb.erb +0 -0
  267. /data/{deprecated_generators → lib/generators}/graphiti/templates/update_request_spec.rb.erb +0 -0
@@ -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
  #
@@ -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.3"
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
+ )
data/lib/graphiti.rb CHANGED
@@ -3,6 +3,12 @@ require "forwardable"
3
3
  require "uri"
4
4
  require "ostruct" unless defined?(::OpenStruct)
5
5
 
6
+ # The umbrella require, not just the core_ext files below: Graphiti.broadcast
7
+ # calls ActiveSupport::Notifications.instrument, which reaches for
8
+ # ActiveSupport::IsolatedExecutionState. That constant is autoloaded here and
9
+ # nowhere else, so without this a non-Rails app raises NameError on the first
10
+ # render unless something else happened to load ActiveSupport for us.
11
+ require "active_support"
6
12
  require "active_support/version"
7
13
  require "active_support/deprecation"
8
14
  require "active_support/deprecator" if ::ActiveSupport.version >= Gem::Version.new("7.1")
@@ -14,12 +20,34 @@ require "active_support/concern"
14
20
  require "active_support/time"
15
21
 
16
22
  require "dry-types"
17
- require "graphiti_errors"
18
23
 
19
24
  require "jsonapi/serializable"
20
25
 
26
+ # These gems are merged into graphiti as of 2.0, but their 1.x/0.x releases
27
+ # still resolve against graphiti 2.x. Leaving one installed is never harmless:
28
+ # graphiti_spec_helpers and graphiti-rails ship files that collide with ours
29
+ # (lib/graphiti_spec_helpers.rb, lib/graphiti/rails.rb), so which copy a require
30
+ # picks up comes down to load path order, and graphiti_errors installs a second,
31
+ # competing exception handler on any controller that includes it. Fail loudly
32
+ # rather than let either happen quietly. Checked here because graphiti is loaded
33
+ # either way, whichever copy wins.
34
+ {
35
+ "graphiti_spec_helpers" => 'The "graphiti_spec_helpers/rspec" require and the GraphitiSpecHelpers namespace are unchanged.',
36
+ "graphiti-rails" => 'Graphiti::Rails and its config.graphiti options are unchanged. Drop the "graphiti-rails" require if you have one.',
37
+ "graphiti_errors" => "Exception handling now goes through rescue_registry. Remove `include GraphitiErrors` from your controllers — Graphiti registers its own handlers, and you can add yours with `register_exception`."
38
+ }.each do |gem_name, guidance|
39
+ next unless Gem.loaded_specs.key?(gem_name)
40
+
41
+ raise <<~MSG
42
+ #{gem_name} is merged into graphiti as of 2.0 and is no longer published
43
+ separately. Remove it from your Gemfile.
44
+
45
+ #{guidance}
46
+ MSG
47
+ end
48
+
21
49
  module Graphiti
22
- DEPRECATOR = ActiveSupport::Deprecation.new("2.0", "Graphiti")
50
+ DEPRECATOR = ActiveSupport::Deprecation.new("3.0", "Graphiti")
23
51
 
24
52
  # @api private
25
53
  def self.context
@@ -203,6 +231,10 @@ require "graphiti/query"
203
231
  require "graphiti/debugger"
204
232
  require "graphiti/util/cache_debug"
205
233
  require "graphiti/util/uri_decoder"
234
+ require "graphiti/error_serializers/validation"
235
+ require "graphiti/error_serializers/invalid_request"
236
+ require "graphiti/error_serializers/conflict_request"
237
+ require "graphiti/error_serializers/deprecated_constants"
206
238
 
207
239
  if defined?(ActiveRecord)
208
240
  require "graphiti/adapters/active_record"
@@ -210,14 +242,6 @@ end
210
242
 
211
243
  if defined?(Rails)
212
244
  require "graphiti/rails"
213
- require "graphiti/responders"
214
-
215
- # graphiti-rails has own Railtie
216
- begin
217
- require "graphiti-rails"
218
- rescue LoadError
219
- require "graphiti/railtie"
220
- end
221
245
  end
222
246
 
223
247
  require "graphiti/runner"
@@ -0,0 +1,11 @@
1
+ # Deprecated entry point. graphiti_errors was absorbed into graphiti in 2.0 and
2
+ # graphiti required it for you in 1.x, so an explicit require left in an
3
+ # initializer or spec_helper would otherwise raise LoadError. Only the
4
+ # serializers survived. See graphiti/error_serializers/deprecated_constants.rb.
5
+ # Remove in 3.0.
6
+ require "graphiti"
7
+
8
+ Graphiti::DEPRECATOR.warn(
9
+ 'require "graphiti_errors" is no longer needed. graphiti_errors is part of graphiti as of 2.0, ' \
10
+ "and its exception handling is replaced by rescue_registry."
11
+ )
@@ -0,0 +1,3 @@
1
+ # Deprecated entry point. See graphiti_spec_helpers.rb. Remove in 3.0.
2
+ require "graphiti/spec_helpers/rspec"
3
+ require "graphiti_spec_helpers"
@@ -0,0 +1,11 @@
1
+ # Deprecated entry point. The spec helpers moved under Graphiti::SpecHelpers
2
+ # when the graphiti_spec_helpers gem was absorbed into graphiti in 2.0. This
3
+ # keeps `require "graphiti_spec_helpers"` and the old constant working.
4
+ # Remove in 3.0.
5
+ require "graphiti/spec_helpers"
6
+
7
+ GraphitiSpecHelpers = ActiveSupport::Deprecation::DeprecatedConstantProxy.new(
8
+ "GraphitiSpecHelpers",
9
+ "Graphiti::SpecHelpers",
10
+ Graphiti::DEPRECATOR
11
+ )