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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +30 -86
- data/.github/workflows/docs.yml +60 -0
- data/.github/workflows/release.yml +8 -8
- data/.gitignore +7 -0
- data/.npmrc +9 -0
- data/.standard.yml +4 -1
- data/Appraisals +33 -32
- data/CHANGELOG.md +41 -0
- data/README.md +13 -2
- data/UPGRADING.md +2 -68
- data/docs/concepts/backends-and-models.md +122 -0
- data/docs/concepts/endpoints.md +183 -0
- data/docs/concepts/links.md +212 -0
- data/docs/concepts/overview.md +80 -0
- data/docs/concepts/persisting.md +376 -0
- data/docs/concepts/relationships.md +527 -0
- data/docs/concepts/resources.md +677 -0
- data/docs/getting-started/first-api.md +289 -0
- data/docs/getting-started/installation.md +185 -0
- data/docs/intro.md +307 -0
- data/docs/js/authentication.md +63 -0
- data/docs/js/ddau.md +20 -0
- data/docs/js/extra-params.md +41 -0
- data/docs/js/index.md +112 -0
- data/docs/js/installation.md +120 -0
- data/docs/js/middleware.md +72 -0
- data/docs/js/models.md +202 -0
- data/docs/js/reads.md +494 -0
- data/docs/js/state-syncing.md +100 -0
- data/docs/js/writes.md +373 -0
- data/docs/reference/vandal.md +63 -0
- data/docs/reference/why.md +13 -0
- data/docs/topics/authorization.md +155 -0
- data/docs/topics/caching.md +55 -0
- data/docs/topics/customizing-sideloads.md +156 -0
- data/docs/topics/debugging.md +216 -0
- data/docs/topics/error-handling.md +210 -0
- data/docs/topics/etags.md +46 -0
- data/docs/topics/hopping-relationships.md +149 -0
- data/docs/topics/json-attributes.md +77 -0
- data/docs/topics/openstruct-models.md +50 -0
- data/docs/topics/remote-resources.md +291 -0
- data/docs/topics/testing.md +894 -0
- data/docs/topics/without-activerecord.md +324 -0
- data/docs/tutorial/index.md +58 -0
- data/docs/tutorial/step_0.md +107 -0
- data/docs/tutorial/step_1.md +199 -0
- data/docs/tutorial/step_2.md +312 -0
- data/docs/tutorial/step_3.md +142 -0
- data/docs/tutorial/step_4.md +135 -0
- data/docs/tutorial/step_5.md +69 -0
- data/docs/tutorial/step_6.md +82 -0
- data/docs/tutorial/step_7.md +205 -0
- data/docs/tutorial/step_8.md +128 -0
- data/docs/tutorial/step_9.md +171 -0
- data/docs/upgrading.md +265 -0
- data/gemfiles/rails_7_1.gemfile +4 -3
- data/gemfiles/{rails_7_2_graphiti_rails.gemfile → rails_7_2.gemfile} +3 -3
- data/gemfiles/{rails_8_1_graphiti_rails.gemfile → rails_8_0.gemfile} +3 -3
- data/gemfiles/{rails_8_0_graphiti_rails.gemfile → rails_8_1.gemfile} +3 -3
- data/graphiti.gemspec +7 -5
- data/{deprecated_generators → lib/generators}/graphiti/api_test_generator.rb +7 -1
- data/{deprecated_generators → lib/generators}/graphiti/generator_mixin.rb +14 -1
- data/{deprecated_generators → lib/generators}/graphiti/install_generator.rb +19 -13
- data/{deprecated_generators → lib/generators}/graphiti/resource_generator.rb +43 -6
- data/{deprecated_generators → lib/generators}/graphiti/templates/index_request_spec.rb.erb +1 -1
- data/{deprecated_generators → lib/generators}/graphiti/templates/resource_reads_spec.rb.erb +6 -6
- data/{deprecated_generators → lib/generators}/graphiti/templates/show_request_spec.rb.erb +1 -1
- data/lib/graphiti/configuration.rb +2 -2
- data/lib/graphiti/error_serializers/conflict_request.rb +19 -0
- data/lib/graphiti/error_serializers/deprecated_constants.rb +48 -0
- data/lib/graphiti/error_serializers/invalid_request.rb +56 -0
- data/lib/graphiti/error_serializers/validation.rb +143 -0
- data/lib/graphiti/errors.rb +4 -23
- data/lib/graphiti/query.rb +1 -1
- data/lib/graphiti/rails/context.rb +33 -0
- data/lib/graphiti/rails/controller.rb +41 -0
- data/lib/graphiti/rails/debugging.rb +18 -0
- data/lib/graphiti/rails/exception_handlers.rb +77 -0
- data/lib/graphiti/rails/railtie.rb +139 -0
- data/lib/graphiti/rails/responders.rb +21 -0
- data/lib/graphiti/rails/test_helpers.rb +22 -0
- data/lib/graphiti/rails.rb +47 -29
- data/lib/graphiti/resource/configuration.rb +1 -0
- data/lib/graphiti/resource/interface.rb +2 -2
- data/lib/graphiti/resource/persistence.rb +14 -2
- data/lib/graphiti/resource/remote.rb +2 -2
- data/lib/graphiti/resource/sideloading.rb +1 -1
- data/lib/graphiti/resource.rb +13 -1
- data/lib/graphiti/responders.rb +7 -20
- data/lib/graphiti/schema.rb +5 -1
- data/lib/graphiti/schema_diff.rb +4 -0
- data/lib/graphiti/scope.rb +45 -37
- data/lib/graphiti/serializer.rb +6 -0
- data/lib/graphiti/sideload/belongs_to.rb +38 -5
- data/lib/graphiti/sideload/polymorphic_belongs_to.rb +27 -23
- data/lib/graphiti/sideload.rb +54 -35
- data/lib/graphiti/spec_helpers/errors.rb +73 -0
- data/lib/graphiti/spec_helpers/errors_proxy.rb +75 -0
- data/lib/graphiti/spec_helpers/helpers.rb +107 -0
- data/lib/graphiti/spec_helpers/node.rb +88 -0
- data/lib/graphiti/spec_helpers/rspec.rb +147 -0
- data/lib/graphiti/spec_helpers.rb +53 -0
- data/lib/graphiti/util/include_params.rb +2 -2
- data/lib/graphiti/util/persistence.rb +10 -11
- data/lib/graphiti/util/serializer_relationships.rb +41 -5
- data/lib/graphiti/version.rb +1 -1
- data/lib/graphiti-rails.rb +11 -0
- data/lib/graphiti.rb +34 -10
- data/lib/graphiti_errors.rb +11 -0
- data/lib/graphiti_spec_helpers/rspec.rb +3 -0
- data/lib/graphiti_spec_helpers.rb +11 -0
- data/lib/{graphiti/deprecated_tasks.rb → tasks/graphiti.rake} +6 -1
- data/package-lock.json +6199 -0
- data/package.json +5 -4
- data/website/.gitignore +20 -0
- data/website/README.md +43 -0
- data/website/docusaurus.config.js +141 -0
- data/website/package-lock.json +19474 -0
- data/website/package.json +46 -0
- data/website/sidebars.js +82 -0
- data/website/src/css/custom.css +58 -0
- data/website/src/pages/markdown-page.mdx +7 -0
- data/website/static/.nojekyll +0 -0
- data/website/static/1.13/2019/03/31/graphiti-1-0.html +205 -0
- data/website/static/1.13/2019/05/08/graphiti-1-1.html +212 -0
- data/website/static/1.13/2019/05/20/graphiti-1-2.html +214 -0
- data/website/static/1.13/2019/10/14/tutorial.html +198 -0
- data/website/static/1.13/CNAME +1 -0
- data/website/static/1.13/README.md +16 -0
- data/website/static/1.13/assets/css/syntax.css +60 -0
- data/website/static/1.13/assets/favicons/android-chrome-192x192.png +0 -0
- data/website/static/1.13/assets/favicons/android-chrome-256x256.png +0 -0
- data/website/static/1.13/assets/favicons/apple-touch-icon.png +0 -0
- data/website/static/1.13/assets/favicons/browserconfig.xml +9 -0
- data/website/static/1.13/assets/favicons/favicon-16x16.png +0 -0
- data/website/static/1.13/assets/favicons/favicon-32x32.png +0 -0
- data/website/static/1.13/assets/favicons/favicon.ico +0 -0
- data/website/static/1.13/assets/favicons/mstile-150x150.png +0 -0
- data/website/static/1.13/assets/favicons/safari-pinned-tab.svg +1 -0
- data/website/static/1.13/assets/favicons/site.webmanifest +19 -0
- data/website/static/1.13/assets/img/backend.gif +0 -0
- data/website/static/1.13/assets/img/conformity.png +0 -0
- data/website/static/1.13/assets/img/error_payload.png +0 -0
- data/website/static/1.13/assets/img/gh.png +0 -0
- data/website/static/1.13/assets/img/lifecycle.gif +0 -0
- data/website/static/1.13/assets/img/logo-500.png +0 -0
- data/website/static/1.13/assets/img/logo.png +0 -0
- data/website/static/1.13/assets/img/love-graffiti.jpg +0 -0
- data/website/static/1.13/assets/img/meta_total_count.png +0 -0
- data/website/static/1.13/assets/img/persist.jpg +0 -0
- data/website/static/1.13/assets/img/resource.gif +0 -0
- data/website/static/1.13/assets/img/rest-graffiti.jpg +0 -0
- data/website/static/1.13/assets/img/rest1.gif +0 -0
- data/website/static/1.13/assets/img/rest2.gif +0 -0
- data/website/static/1.13/assets/img/rest3.gif +0 -0
- data/website/static/1.13/assets/img/rethink-rest-graffiti.jpg +0 -0
- data/website/static/1.13/assets/img/why.png +0 -0
- data/website/static/1.13/assets/js/highlight.pack.js +2 -0
- data/website/static/1.13/assets/main.css +15518 -0
- data/website/static/1.13/assets/main.css.map +1 -0
- data/website/static/1.13/bin/bundle +109 -0
- data/website/static/1.13/bin/jekyll +27 -0
- data/website/static/1.13/bin/kramdown +27 -0
- data/website/static/1.13/bin/listen +27 -0
- data/website/static/1.13/bin/rake +27 -0
- data/website/static/1.13/bin/rougify +27 -0
- data/website/static/1.13/bin/safe_yaml +27 -0
- data/website/static/1.13/bin/sass +27 -0
- data/website/static/1.13/bin/sass-convert +27 -0
- data/website/static/1.13/bin/scss +27 -0
- data/website/static/1.13/blog.html +259 -0
- data/website/static/1.13/cheatsheet.html +316 -0
- data/website/static/1.13/cookbooks/authorization.md +0 -0
- data/website/static/1.13/cookbooks/caching.md +0 -0
- data/website/static/1.13/cookbooks/customizing-sideloads.html +325 -0
- data/website/static/1.13/cookbooks/etags.md +0 -0
- data/website/static/1.13/cookbooks/hopping-relationships.html +324 -0
- data/website/static/1.13/cookbooks/json_attributes.md +0 -0
- data/website/static/1.13/cookbooks/openstruct-models.md +0 -0
- data/website/static/1.13/cookbooks/remote-resources.md +0 -0
- data/website/static/1.13/cookbooks/without-activerecord.html +510 -0
- data/website/static/1.13/features.html +249 -0
- data/website/static/1.13/feed.xml +106 -0
- data/website/static/1.13/guides/concepts/backends-and-models.html +467 -0
- data/website/static/1.13/guides/concepts/debugging.html +440 -0
- data/website/static/1.13/guides/concepts/endpoints.html +432 -0
- data/website/static/1.13/guides/concepts/error-handling.html +396 -0
- data/website/static/1.13/guides/concepts/links.html +501 -0
- data/website/static/1.13/guides/concepts/remote-resources.html +536 -0
- data/website/static/1.13/guides/concepts/resources.html +2176 -0
- data/website/static/1.13/guides/concepts/testing.html +1469 -0
- data/website/static/1.13/guides/getting-started/installation.html +420 -0
- data/website/static/1.13/guides/graphiti-rails-migration.html +242 -0
- data/website/static/1.13/guides/index.html +269 -0
- data/website/static/1.13/guides/overview.html +325 -0
- data/website/static/1.13/guides/upgrading-2-0.html +193 -0
- data/website/static/1.13/guides/upgrading.html +314 -0
- data/website/static/1.13/guides/vandal.html +282 -0
- data/website/static/1.13/guides/why.html +1121 -0
- data/website/static/1.13/index.html +72 -0
- data/website/static/1.13/js/authentication.html +295 -0
- data/website/static/1.13/js/ddau.html +238 -0
- data/website/static/1.13/js/extra-params.html +270 -0
- data/website/static/1.13/js/index.html +321 -0
- data/website/static/1.13/js/installation.html +637 -0
- data/website/static/1.13/js/introduction.html +257 -0
- data/website/static/1.13/js/middleware.html +318 -0
- data/website/static/1.13/js/reads/fieldsets.html +271 -0
- data/website/static/1.13/js/reads/filtering.html +289 -0
- data/website/static/1.13/js/reads/includes.html +260 -0
- data/website/static/1.13/js/reads/index.html +497 -0
- data/website/static/1.13/js/reads/nested-queries.html +353 -0
- data/website/static/1.13/js/reads/pagination.html +260 -0
- data/website/static/1.13/js/reads/sorting.html +265 -0
- data/website/static/1.13/js/reads/statistics.html +289 -0
- data/website/static/1.13/js/state-syncing.html +340 -0
- data/website/static/1.13/js/writes/deferred.html +296 -0
- data/website/static/1.13/js/writes/dirty-tracking.html +399 -0
- data/website/static/1.13/js/writes/index.html +391 -0
- data/website/static/1.13/js/writes/nested.html +330 -0
- data/website/static/1.13/js/writes/validations.html +272 -0
- data/website/static/1.13/quickstart.html +660 -0
- data/website/static/1.13/template +161 -0
- data/website/static/1.13/tutorial/index.html +250 -0
- data/website/static/1.13/tutorial/step_0.html +292 -0
- data/website/static/1.13/tutorial/step_1.html +517 -0
- data/website/static/1.13/tutorial/step_2.html +481 -0
- data/website/static/1.13/tutorial/step_3.html +323 -0
- data/website/static/1.13/tutorial/step_4.html +318 -0
- data/website/static/1.13/tutorial/step_5.html +265 -0
- data/website/static/1.13/tutorial/step_6.html +276 -0
- data/website/static/1.13/tutorial/step_7.html +390 -0
- data/website/static/1.13/tutorial/step_8.html +316 -0
- data/website/static/1.13/tutorial/step_9.html +365 -0
- data/website/static/assets/img/error_payload.png +0 -0
- data/website/static/assets/img/legacy/legacy-0378a3bb39.png +0 -0
- data/website/static/assets/img/legacy/legacy-05bbd3e5fd.png +0 -0
- data/website/static/assets/img/legacy/legacy-07aa104495.png +0 -0
- data/website/static/assets/img/legacy/legacy-0c75a16b3a.gif +0 -0
- data/website/static/assets/img/legacy/legacy-3076df6209.png +0 -0
- data/website/static/assets/img/legacy/legacy-7f6889bc89.png +0 -0
- data/website/static/assets/img/legacy/legacy-a2cc4363c3.png +0 -0
- data/website/static/assets/img/legacy/legacy-f67cfa89ab.png +0 -0
- data/website/static/assets/img/meta_total_count.png +0 -0
- data/website/static/img/docusaurus-social-card.jpg +0 -0
- data/website/static/img/docusaurus.png +0 -0
- data/website/static/img/favicon.ico +0 -0
- data/website/static/img/logo.png +0 -0
- data/website/static/img/logo.svg +1 -0
- data/website/static/img/undraw_docusaurus_mountain.svg +171 -0
- data/website/static/img/undraw_docusaurus_react.svg +170 -0
- data/website/static/img/undraw_docusaurus_tree.svg +40 -0
- metadata +245 -46
- data/gemfiles/rails_6.gemfile +0 -18
- data/gemfiles/rails_6_graphiti_rails.gemfile +0 -19
- data/gemfiles/rails_7.gemfile +0 -18
- data/gemfiles/rails_7_1_graphiti_rails.gemfile +0 -19
- data/gemfiles/rails_7_graphiti_rails.gemfile +0 -19
- data/lib/graphiti/railtie.rb +0 -121
- /data/{deprecated_generators → lib/generators}/graphiti/resource_test_generator.rb +0 -0
- /data/{deprecated_generators → lib/generators}/graphiti/templates/application_resource.rb.erb +0 -0
- /data/{deprecated_generators → lib/generators}/graphiti/templates/controller.rb.erb +0 -0
- /data/{deprecated_generators → lib/generators}/graphiti/templates/create_request_spec.rb.erb +0 -0
- /data/{deprecated_generators → lib/generators}/graphiti/templates/destroy_request_spec.rb.erb +0 -0
- /data/{deprecated_generators → lib/generators}/graphiti/templates/resource.rb.erb +0 -0
- /data/{deprecated_generators → lib/generators}/graphiti/templates/resource_writes_spec.rb.erb +0 -0
- /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
|
|
10
|
+
# But the controller had this code:
|
|
11
11
|
#
|
|
12
|
-
# sideload_allowlist
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
57
|
-
#
|
|
58
|
-
|
|
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
|
|
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
|
data/lib/graphiti/version.rb
CHANGED
|
@@ -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
|
+
)
|