graphiti 2.0.0.beta.3 → 2.0.0.beta.5

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 (274) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +30 -86
  3. data/.github/workflows/docs.yml +57 -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 +48 -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 +260 -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 -23
  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 +68 -7
  67. data/{deprecated_generators → lib/generators}/graphiti/templates/controller.rb.erb +1 -1
  68. data/{deprecated_generators → lib/generators}/graphiti/templates/index_request_spec.rb.erb +1 -1
  69. data/{deprecated_generators → lib/generators}/graphiti/templates/resource_reads_spec.rb.erb +6 -6
  70. data/{deprecated_generators → lib/generators}/graphiti/templates/show_request_spec.rb.erb +1 -1
  71. data/lib/graphiti/configuration.rb +2 -2
  72. data/lib/graphiti/error_serializers/conflict_request.rb +19 -0
  73. data/lib/graphiti/error_serializers/deprecated_constants.rb +48 -0
  74. data/lib/graphiti/error_serializers/invalid_request.rb +56 -0
  75. data/lib/graphiti/error_serializers/validation.rb +143 -0
  76. data/lib/graphiti/errors.rb +4 -4
  77. data/lib/graphiti/query.rb +1 -1
  78. data/lib/graphiti/rails/context.rb +33 -0
  79. data/lib/graphiti/rails/controller.rb +41 -0
  80. data/lib/graphiti/rails/debugging.rb +18 -0
  81. data/lib/graphiti/rails/exception_handlers.rb +77 -0
  82. data/lib/graphiti/rails/railtie.rb +139 -0
  83. data/lib/graphiti/rails/rake_helpers.rb +42 -0
  84. data/lib/graphiti/rails/responders.rb +21 -0
  85. data/lib/graphiti/rails/test_helpers.rb +22 -0
  86. data/lib/graphiti/rails.rb +47 -29
  87. data/lib/graphiti/resource/configuration.rb +1 -0
  88. data/lib/graphiti/resource/interface.rb +2 -2
  89. data/lib/graphiti/resource/remote.rb +2 -2
  90. data/lib/graphiti/resource/sideloading.rb +1 -1
  91. data/lib/graphiti/resource.rb +13 -1
  92. data/lib/graphiti/responders.rb +7 -20
  93. data/lib/graphiti/schema.rb +5 -1
  94. data/lib/graphiti/schema_diff.rb +4 -0
  95. data/lib/graphiti/scope.rb +45 -37
  96. data/lib/graphiti/serializer.rb +6 -0
  97. data/lib/graphiti/sideload/belongs_to.rb +38 -5
  98. data/lib/graphiti/sideload/polymorphic_belongs_to.rb +27 -23
  99. data/lib/graphiti/sideload.rb +54 -35
  100. data/lib/graphiti/spec_helpers/errors.rb +73 -0
  101. data/lib/graphiti/spec_helpers/errors_proxy.rb +75 -0
  102. data/lib/graphiti/spec_helpers/helpers.rb +107 -0
  103. data/lib/graphiti/spec_helpers/matchers.rb +196 -0
  104. data/lib/graphiti/spec_helpers/node.rb +88 -0
  105. data/lib/graphiti/spec_helpers/rspec.rb +148 -0
  106. data/lib/graphiti/spec_helpers.rb +54 -0
  107. data/lib/graphiti/util/include_params.rb +2 -2
  108. data/lib/graphiti/util/serializer_relationships.rb +41 -5
  109. data/lib/graphiti/version.rb +1 -1
  110. data/lib/graphiti-rails.rb +11 -0
  111. data/lib/graphiti.rb +34 -10
  112. data/lib/graphiti_errors.rb +11 -0
  113. data/lib/graphiti_spec_helpers/rspec.rb +3 -0
  114. data/lib/graphiti_spec_helpers.rb +11 -0
  115. data/lib/tasks/graphiti.rake +29 -0
  116. data/package-lock.json +6199 -0
  117. data/package.json +5 -4
  118. data/website/.gitignore +20 -0
  119. data/website/README.md +43 -0
  120. data/website/docusaurus.config.js +197 -0
  121. data/website/package-lock.json +19474 -0
  122. data/website/package.json +46 -0
  123. data/website/sidebars.js +82 -0
  124. data/website/src/css/custom.css +85 -0
  125. data/website/src/pages/markdown-page.mdx +7 -0
  126. data/website/static/.nojekyll +0 -0
  127. data/website/static/1.13/2019/03/31/graphiti-1-0.html +205 -0
  128. data/website/static/1.13/2019/05/08/graphiti-1-1.html +212 -0
  129. data/website/static/1.13/2019/05/20/graphiti-1-2.html +214 -0
  130. data/website/static/1.13/2019/10/14/tutorial.html +198 -0
  131. data/website/static/1.13/CNAME +1 -0
  132. data/website/static/1.13/README.md +16 -0
  133. data/website/static/1.13/assets/css/syntax.css +60 -0
  134. data/website/static/1.13/assets/favicons/android-chrome-192x192.png +0 -0
  135. data/website/static/1.13/assets/favicons/android-chrome-256x256.png +0 -0
  136. data/website/static/1.13/assets/favicons/apple-touch-icon.png +0 -0
  137. data/website/static/1.13/assets/favicons/browserconfig.xml +9 -0
  138. data/website/static/1.13/assets/favicons/favicon-16x16.png +0 -0
  139. data/website/static/1.13/assets/favicons/favicon-32x32.png +0 -0
  140. data/website/static/1.13/assets/favicons/favicon.ico +0 -0
  141. data/website/static/1.13/assets/favicons/mstile-150x150.png +0 -0
  142. data/website/static/1.13/assets/favicons/safari-pinned-tab.svg +1 -0
  143. data/website/static/1.13/assets/favicons/site.webmanifest +19 -0
  144. data/website/static/1.13/assets/img/backend.gif +0 -0
  145. data/website/static/1.13/assets/img/conformity.png +0 -0
  146. data/website/static/1.13/assets/img/error_payload.png +0 -0
  147. data/website/static/1.13/assets/img/fancy-cushion.png +0 -0
  148. data/website/static/1.13/assets/img/gh.png +0 -0
  149. data/website/static/1.13/assets/img/home-bg.jpg +0 -0
  150. data/website/static/1.13/assets/img/lifecycle.gif +0 -0
  151. data/website/static/1.13/assets/img/logo-500.png +0 -0
  152. data/website/static/1.13/assets/img/logo.png +0 -0
  153. data/website/static/1.13/assets/img/love-graffiti.jpg +0 -0
  154. data/website/static/1.13/assets/img/meta_total_count.png +0 -0
  155. data/website/static/1.13/assets/img/persist.jpg +0 -0
  156. data/website/static/1.13/assets/img/resource.gif +0 -0
  157. data/website/static/1.13/assets/img/rest-graffiti.jpg +0 -0
  158. data/website/static/1.13/assets/img/rest1.gif +0 -0
  159. data/website/static/1.13/assets/img/rest2.gif +0 -0
  160. data/website/static/1.13/assets/img/rest3.gif +0 -0
  161. data/website/static/1.13/assets/img/rethink-rest-graffiti.jpg +0 -0
  162. data/website/static/1.13/assets/img/sunrise.png +0 -0
  163. data/website/static/1.13/assets/img/why.png +0 -0
  164. data/website/static/1.13/assets/js/highlight.pack.js +2 -0
  165. data/website/static/1.13/assets/main.css +15518 -0
  166. data/website/static/1.13/assets/main.css.map +1 -0
  167. data/website/static/1.13/bin/bundle +109 -0
  168. data/website/static/1.13/bin/jekyll +27 -0
  169. data/website/static/1.13/bin/kramdown +27 -0
  170. data/website/static/1.13/bin/listen +27 -0
  171. data/website/static/1.13/bin/rake +27 -0
  172. data/website/static/1.13/bin/rougify +27 -0
  173. data/website/static/1.13/bin/safe_yaml +27 -0
  174. data/website/static/1.13/bin/sass +27 -0
  175. data/website/static/1.13/bin/sass-convert +27 -0
  176. data/website/static/1.13/bin/scss +27 -0
  177. data/website/static/1.13/blog.html +259 -0
  178. data/website/static/1.13/cheatsheet.html +316 -0
  179. data/website/static/1.13/cookbooks/authorization.md +0 -0
  180. data/website/static/1.13/cookbooks/caching.md +0 -0
  181. data/website/static/1.13/cookbooks/customizing-sideloads.html +325 -0
  182. data/website/static/1.13/cookbooks/etags.md +0 -0
  183. data/website/static/1.13/cookbooks/hopping-relationships.html +324 -0
  184. data/website/static/1.13/cookbooks/json_attributes.md +0 -0
  185. data/website/static/1.13/cookbooks/openstruct-models.md +0 -0
  186. data/website/static/1.13/cookbooks/remote-resources.md +0 -0
  187. data/website/static/1.13/cookbooks/without-activerecord.html +510 -0
  188. data/website/static/1.13/features.html +249 -0
  189. data/website/static/1.13/feed.xml +106 -0
  190. data/website/static/1.13/guides/concepts/backends-and-models.html +467 -0
  191. data/website/static/1.13/guides/concepts/debugging.html +440 -0
  192. data/website/static/1.13/guides/concepts/endpoints.html +432 -0
  193. data/website/static/1.13/guides/concepts/error-handling.html +396 -0
  194. data/website/static/1.13/guides/concepts/links.html +501 -0
  195. data/website/static/1.13/guides/concepts/remote-resources.html +536 -0
  196. data/website/static/1.13/guides/concepts/resources.html +2176 -0
  197. data/website/static/1.13/guides/concepts/testing.html +1469 -0
  198. data/website/static/1.13/guides/getting-started/installation.html +420 -0
  199. data/website/static/1.13/guides/graphiti-rails-migration.html +242 -0
  200. data/website/static/1.13/guides/index.html +269 -0
  201. data/website/static/1.13/guides/overview.html +325 -0
  202. data/website/static/1.13/guides/upgrading-2-0.html +193 -0
  203. data/website/static/1.13/guides/upgrading.html +314 -0
  204. data/website/static/1.13/guides/vandal.html +282 -0
  205. data/website/static/1.13/guides/why.html +1121 -0
  206. data/website/static/1.13/index.html +72 -0
  207. data/website/static/1.13/js/authentication.html +295 -0
  208. data/website/static/1.13/js/ddau.html +238 -0
  209. data/website/static/1.13/js/extra-params.html +270 -0
  210. data/website/static/1.13/js/index.html +321 -0
  211. data/website/static/1.13/js/installation.html +637 -0
  212. data/website/static/1.13/js/introduction.html +257 -0
  213. data/website/static/1.13/js/middleware.html +318 -0
  214. data/website/static/1.13/js/reads/fieldsets.html +271 -0
  215. data/website/static/1.13/js/reads/filtering.html +289 -0
  216. data/website/static/1.13/js/reads/includes.html +260 -0
  217. data/website/static/1.13/js/reads/index.html +497 -0
  218. data/website/static/1.13/js/reads/nested-queries.html +353 -0
  219. data/website/static/1.13/js/reads/pagination.html +260 -0
  220. data/website/static/1.13/js/reads/sorting.html +265 -0
  221. data/website/static/1.13/js/reads/statistics.html +289 -0
  222. data/website/static/1.13/js/state-syncing.html +340 -0
  223. data/website/static/1.13/js/writes/deferred.html +296 -0
  224. data/website/static/1.13/js/writes/dirty-tracking.html +399 -0
  225. data/website/static/1.13/js/writes/index.html +391 -0
  226. data/website/static/1.13/js/writes/nested.html +330 -0
  227. data/website/static/1.13/js/writes/validations.html +272 -0
  228. data/website/static/1.13/quickstart.html +660 -0
  229. data/website/static/1.13/template +161 -0
  230. data/website/static/1.13/tutorial/index.html +250 -0
  231. data/website/static/1.13/tutorial/step_0.html +292 -0
  232. data/website/static/1.13/tutorial/step_1.html +517 -0
  233. data/website/static/1.13/tutorial/step_2.html +481 -0
  234. data/website/static/1.13/tutorial/step_3.html +323 -0
  235. data/website/static/1.13/tutorial/step_4.html +318 -0
  236. data/website/static/1.13/tutorial/step_5.html +265 -0
  237. data/website/static/1.13/tutorial/step_6.html +276 -0
  238. data/website/static/1.13/tutorial/step_7.html +390 -0
  239. data/website/static/1.13/tutorial/step_8.html +316 -0
  240. data/website/static/1.13/tutorial/step_9.html +365 -0
  241. data/website/static/CNAME +1 -0
  242. data/website/static/assets/img/error_payload.png +0 -0
  243. data/website/static/assets/img/legacy/legacy-0378a3bb39.png +0 -0
  244. data/website/static/assets/img/legacy/legacy-05bbd3e5fd.png +0 -0
  245. data/website/static/assets/img/legacy/legacy-07aa104495.png +0 -0
  246. data/website/static/assets/img/legacy/legacy-0c75a16b3a.gif +0 -0
  247. data/website/static/assets/img/legacy/legacy-3076df6209.png +0 -0
  248. data/website/static/assets/img/legacy/legacy-7f6889bc89.png +0 -0
  249. data/website/static/assets/img/legacy/legacy-a2cc4363c3.png +0 -0
  250. data/website/static/assets/img/legacy/legacy-f67cfa89ab.png +0 -0
  251. data/website/static/assets/img/meta_total_count.png +0 -0
  252. data/website/static/img/docusaurus-social-card.jpg +0 -0
  253. data/website/static/img/docusaurus.png +0 -0
  254. data/website/static/img/favicon.ico +0 -0
  255. data/website/static/img/logo.png +0 -0
  256. data/website/static/img/logo.svg +1 -0
  257. data/website/static/img/undraw_docusaurus_mountain.svg +171 -0
  258. data/website/static/img/undraw_docusaurus_react.svg +170 -0
  259. data/website/static/img/undraw_docusaurus_tree.svg +40 -0
  260. metadata +251 -54
  261. data/gemfiles/rails_6.gemfile +0 -18
  262. data/gemfiles/rails_6_graphiti_rails.gemfile +0 -19
  263. data/gemfiles/rails_7.gemfile +0 -18
  264. data/gemfiles/rails_7_1_graphiti_rails.gemfile +0 -19
  265. data/gemfiles/rails_7_graphiti_rails.gemfile +0 -19
  266. data/lib/graphiti/deprecated_tasks.rb +0 -49
  267. data/lib/graphiti/railtie.rb +0 -121
  268. /data/{deprecated_generators → lib/generators}/graphiti/resource_test_generator.rb +0 -0
  269. /data/{deprecated_generators → lib/generators}/graphiti/templates/application_resource.rb.erb +0 -0
  270. /data/{deprecated_generators → lib/generators}/graphiti/templates/create_request_spec.rb.erb +0 -0
  271. /data/{deprecated_generators → lib/generators}/graphiti/templates/destroy_request_spec.rb.erb +0 -0
  272. /data/{deprecated_generators → lib/generators}/graphiti/templates/resource.rb.erb +0 -0
  273. /data/{deprecated_generators → lib/generators}/graphiti/templates/resource_writes_spec.rb.erb +0 -0
  274. /data/{deprecated_generators → lib/generators}/graphiti/templates/update_request_spec.rb.erb +0 -0
@@ -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,148 @@
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
+ rspec.include Graphiti::SpecHelpers::Matchers, type: :resource
124
+ end
125
+ end
126
+
127
+ def self.schema!(resources = nil)
128
+ ::RSpec.describe "Graphiti Schema" do
129
+ it "generates a backwards-compatible schema" do
130
+ message = <<~MSG
131
+ Found backwards-incompatibilities in schema! Run with FORCE_SCHEMA=true to ignore.
132
+
133
+ Incompatibilities:
134
+
135
+ MSG
136
+
137
+ errors = Graphiti::Schema.generate!(resources)
138
+ errors.each do |e|
139
+ message << "#{e}\n"
140
+ end
141
+
142
+ expect(errors.empty?).to eq(true), message
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,54 @@
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
+ require "graphiti/spec_helpers/matchers"
15
+
16
+ module Graphiti
17
+ module SpecHelpers
18
+ def self.included(klass)
19
+ klass.send(:include, Helpers)
20
+ end
21
+
22
+ class TestRunner < ::Graphiti::Runner
23
+ def current_user
24
+ nil
25
+ end
26
+ end
27
+
28
+ module Sugar
29
+ def d
30
+ jsonapi_data
31
+ end
32
+
33
+ def included(type = nil)
34
+ jsonapi_included(type)
35
+ end
36
+
37
+ def errors
38
+ jsonapi_errors
39
+ end
40
+
41
+ def dt(*args)
42
+ json_datetime(*args)
43
+ end
44
+
45
+ def datetime(*args)
46
+ json_datetime(*args)
47
+ end
48
+
49
+ def date(*args)
50
+ json_date(*args)
51
+ end
52
+ end
53
+ end
54
+ 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.5"
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
+ )
@@ -0,0 +1,29 @@
1
+ require "graphiti/rails/rake_helpers"
2
+
3
+ namespace :graphiti do
4
+ helpers = Graphiti::Rails::RakeHelpers
5
+
6
+ desc "Execute request without web server."
7
+ task :request, [:path, :debug] => [:environment] do |_, args|
8
+ helpers.setup_rails!
9
+ Graphiti.logger = Graphiti.stdout_logger
10
+ Graphiti::Debugger.preserve = true
11
+ require "pp"
12
+ path, debug = args[:path], args[:debug]
13
+ puts "Graphiti Request: #{path}"
14
+ json = helpers.make_request(path, debug)
15
+ pp json
16
+ Graphiti::Debugger.flush if debug
17
+ end
18
+
19
+ desc "Execute benchmark without web server."
20
+ task :benchmark, [:path, :requests] => [:environment] do |_, args|
21
+ helpers.setup_rails!
22
+ took = Benchmark.ms {
23
+ args[:requests].to_i.times do
24
+ helpers.make_request(args[:path])
25
+ end
26
+ }
27
+ puts "Took: #{(took / args[:requests].to_f).round(2)}ms"
28
+ end
29
+ end