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,48 @@
|
|
|
1
|
+
# The graphiti_errors gem was absorbed into graphiti in 2.0. Only its
|
|
2
|
+
# serializers survived, as Graphiti::ErrorSerializers. The rest of it (the
|
|
3
|
+
# GraphitiErrors controller mixin and its exception handlers) is replaced by
|
|
4
|
+
# rescue_registry rather than renamed, so there is deliberately nothing here to
|
|
5
|
+
# alias it to. A custom error renderer reaching for one of the serializers gets
|
|
6
|
+
# pointed at the new name instead of a bare NameError. Remove in 3.0.
|
|
7
|
+
module GraphitiErrors
|
|
8
|
+
# `include GraphitiErrors` in ApplicationController was this gem's headline
|
|
9
|
+
# usage. Including an empty module succeeds, so without this an upgraded app
|
|
10
|
+
# boots clean and then serves Rails' default HTML error pages instead of
|
|
11
|
+
# JSON:API ones, with nothing to indicate why.
|
|
12
|
+
def self.included(klass)
|
|
13
|
+
raise <<~MSG
|
|
14
|
+
GraphitiErrors was merged into graphiti as of 2.0 and `include GraphitiErrors`
|
|
15
|
+
no longer does anything.
|
|
16
|
+
|
|
17
|
+
Exception handling now goes through rescue_registry. Replace it with:
|
|
18
|
+
|
|
19
|
+
include Graphiti::Rails::Controller
|
|
20
|
+
|
|
21
|
+
which registers Graphiti's own handlers. Your own go alongside them with
|
|
22
|
+
`register_exception`, available on every controller.
|
|
23
|
+
MSG
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
module Validation; end
|
|
27
|
+
|
|
28
|
+
module InvalidRequest; end
|
|
29
|
+
|
|
30
|
+
module ConflictRequest; end
|
|
31
|
+
|
|
32
|
+
module Serializers; end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
{
|
|
36
|
+
"GraphitiErrors::Validation::Serializer" => "Graphiti::ErrorSerializers::Validation",
|
|
37
|
+
"GraphitiErrors::InvalidRequest::Serializer" => "Graphiti::ErrorSerializers::InvalidRequest",
|
|
38
|
+
"GraphitiErrors::ConflictRequest::Serializer" => "Graphiti::ErrorSerializers::ConflictRequest",
|
|
39
|
+
# Graphiti 1.0.x referenced this spelling
|
|
40
|
+
"GraphitiErrors::Serializers::Validation" => "Graphiti::ErrorSerializers::Validation"
|
|
41
|
+
}.each do |old_name, new_name|
|
|
42
|
+
namespace, _, constant = old_name.rpartition("::")
|
|
43
|
+
|
|
44
|
+
Object.const_get(namespace).const_set(
|
|
45
|
+
constant,
|
|
46
|
+
ActiveSupport::Deprecation::DeprecatedConstantProxy.new(old_name, new_name, Graphiti::DEPRECATOR)
|
|
47
|
+
)
|
|
48
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module Graphiti
|
|
2
|
+
module ErrorSerializers
|
|
3
|
+
class InvalidRequest
|
|
4
|
+
STATUS = 400
|
|
5
|
+
|
|
6
|
+
attr_reader :source
|
|
7
|
+
|
|
8
|
+
def initialize(source)
|
|
9
|
+
@source = source
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def status
|
|
13
|
+
self.class::STATUS
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def errors
|
|
17
|
+
[].tap do |payload|
|
|
18
|
+
source.details.each_pair do |attribute, attribute_errors|
|
|
19
|
+
attribute_errors.each_with_index do |error, index|
|
|
20
|
+
message = source.messages[attribute][index]
|
|
21
|
+
|
|
22
|
+
payload << {
|
|
23
|
+
code: code,
|
|
24
|
+
status: status.to_s,
|
|
25
|
+
title: title,
|
|
26
|
+
detail: source.full_message(attribute, message),
|
|
27
|
+
source: {pointer: pointer_for(attribute)},
|
|
28
|
+
meta: {
|
|
29
|
+
attribute: attribute,
|
|
30
|
+
message: message,
|
|
31
|
+
code: error[:error]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def code
|
|
42
|
+
"bad_request"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def title
|
|
46
|
+
"Request Error"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# "filter.foo[0]" describes a path into the payload, not an attribute
|
|
50
|
+
# name, so it becomes a JSON pointer.
|
|
51
|
+
def pointer_for(attribute)
|
|
52
|
+
attribute.to_s.tr(".", "/").gsub(/\[(\d+)\]/, '/\1')
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
module Graphiti
|
|
2
|
+
module ErrorSerializers
|
|
3
|
+
# Walks into related objects so nested writes report against the record
|
|
4
|
+
# that actually failed.
|
|
5
|
+
class Validation
|
|
6
|
+
attr_reader :object
|
|
7
|
+
|
|
8
|
+
def initialize(object, relationship_payloads = {}, relationship_meta = {})
|
|
9
|
+
@object = object
|
|
10
|
+
@relationship_payloads = relationship_payloads
|
|
11
|
+
@relationship_meta = relationship_meta
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def attribute_errors
|
|
15
|
+
[].tap do |errors|
|
|
16
|
+
each_error do |attribute, message, code|
|
|
17
|
+
errors << {
|
|
18
|
+
code: "unprocessable_entity",
|
|
19
|
+
status: "422",
|
|
20
|
+
title: "Validation Error",
|
|
21
|
+
detail: detail_for(attribute, message),
|
|
22
|
+
source: {pointer: pointer_for(object, attribute)},
|
|
23
|
+
meta: meta_for(attribute, message, code, @relationship_meta)
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def errors
|
|
30
|
+
return [] unless object.respond_to?(:errors)
|
|
31
|
+
|
|
32
|
+
attribute_errors | relationship_errors(object, @relationship_payloads)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def each_error
|
|
38
|
+
object.errors.messages.each_pair do |attribute, messages|
|
|
39
|
+
details = object.errors.details.find { |k, _| k == attribute }[1]
|
|
40
|
+
|
|
41
|
+
messages.each_with_index do |message, index|
|
|
42
|
+
yield attribute, message, details[index][:error]
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def relationship?(name)
|
|
48
|
+
relationship_names = []
|
|
49
|
+
if activerecord?
|
|
50
|
+
relationship_names = object.class
|
|
51
|
+
.reflect_on_all_associations.map(&:name)
|
|
52
|
+
elsif object.respond_to?(:relationship_names)
|
|
53
|
+
relationship_names = object.relationship_names
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
relationship_names.include?(name)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def attribute?(name)
|
|
60
|
+
object.respond_to?(name)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def meta_for(attribute, message, code, relationship_meta)
|
|
64
|
+
meta = {
|
|
65
|
+
attribute: attribute,
|
|
66
|
+
message: message,
|
|
67
|
+
code: code
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
unless relationship_meta.empty?
|
|
71
|
+
meta = {relationship: meta.merge(relationship_meta)}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
meta
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def detail_for(attribute, message)
|
|
78
|
+
detail = object.errors.full_message(attribute, message)
|
|
79
|
+
detail = message if attribute.to_s.downcase == "base"
|
|
80
|
+
detail
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# @richmolj: Keeping this to support ember-data, but I hate the concept.
|
|
84
|
+
def pointer_for(object, name)
|
|
85
|
+
if relationship?(name)
|
|
86
|
+
"/data/relationships/#{name}"
|
|
87
|
+
elsif attribute?(name)
|
|
88
|
+
"/data/attributes/#{name}"
|
|
89
|
+
elsif name == :base
|
|
90
|
+
nil
|
|
91
|
+
else
|
|
92
|
+
# Probably a nested relation, like post.comments
|
|
93
|
+
"/data/relationships/#{name}"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def activerecord?
|
|
98
|
+
object.class.respond_to?(:reflect_on_all_associations)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def traverse_relationships(model, relationship_params)
|
|
102
|
+
return unless relationship_params
|
|
103
|
+
|
|
104
|
+
relationship_params.each_pair do |name, payload|
|
|
105
|
+
relationship_objects = Array(model.send(name))
|
|
106
|
+
|
|
107
|
+
relationship_objects.each do |relationship_object|
|
|
108
|
+
related_payload = payload
|
|
109
|
+
if payload.is_a?(Array)
|
|
110
|
+
related_payload = payload.find { |p|
|
|
111
|
+
temp_id = relationship_object
|
|
112
|
+
.instance_variable_get(:@_jsonapi_temp_id)
|
|
113
|
+
p[:meta][:temp_id] === temp_id ||
|
|
114
|
+
p[:meta][:id] == relationship_object.id.to_s
|
|
115
|
+
}
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
yield name, relationship_object, related_payload
|
|
119
|
+
relationship_errors(relationship_object, related_payload[:relationships])
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def relationship_errors(model, relationship_payloads)
|
|
125
|
+
errors = []
|
|
126
|
+
traverse_relationships(model, relationship_payloads) do |name, related, payload|
|
|
127
|
+
meta = {}.tap do |hash|
|
|
128
|
+
hash[:name] = name
|
|
129
|
+
hash[:type] = payload[:meta][:jsonapi_type]
|
|
130
|
+
if (temp_id = related.instance_variable_get(:@_jsonapi_temp_id))
|
|
131
|
+
hash[:"temp-id"] = temp_id
|
|
132
|
+
else
|
|
133
|
+
hash[:id] = related.id
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
errors |= self.class.new(related, payload[:relationships], meta).errors
|
|
138
|
+
end
|
|
139
|
+
errors
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
data/lib/graphiti/errors.rb
CHANGED
|
@@ -84,25 +84,6 @@ module Graphiti
|
|
|
84
84
|
end
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
-
class AssignedModelNotSupported < Base
|
|
88
|
-
def initialize(resource_class, method_name)
|
|
89
|
-
@resource_class = resource_class
|
|
90
|
-
@method_name = method_name
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def message
|
|
94
|
-
<<~MSG
|
|
95
|
-
#{@resource_class}: the model was already assigned via #assign_attributes (or #data before save), but ##{@method_name} is overridden without a parameter to receive it, so changes made to the unsaved model would be silently lost.
|
|
96
|
-
|
|
97
|
-
Accept and use the assigned model in your override:
|
|
98
|
-
|
|
99
|
-
def #{@method_name}(attributes, meta = nil, assigned_model: nil)
|
|
100
|
-
|
|
101
|
-
or persist with #save alone, without inspecting the model first. See UPGRADING.md.
|
|
102
|
-
MSG
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
|
|
106
87
|
class RemoteWrite < Base
|
|
107
88
|
def initialize(resource_class)
|
|
108
89
|
@resource_class = resource_class
|
|
@@ -289,7 +270,7 @@ module Graphiti
|
|
|
289
270
|
#{@resource_class.name}: error occurred while sideloading "#{@sideload_name}"!
|
|
290
271
|
|
|
291
272
|
The error was raised while attempting to build query parameters for the associated Resource.
|
|
292
|
-
Read more about sideload scoping here: www.graphiti.dev/
|
|
273
|
+
Read more about sideload scoping here: www.graphiti.dev/concepts/relationships#customizing-scope
|
|
293
274
|
|
|
294
275
|
A good way to debug is to put a debugger within the 'params' block.
|
|
295
276
|
|
|
@@ -313,7 +294,7 @@ module Graphiti
|
|
|
313
294
|
|
|
314
295
|
The error was raised while attempting to build the scope for the associated Resource.
|
|
315
296
|
|
|
316
|
-
Read more about sideload scoping here: www.graphiti.dev/
|
|
297
|
+
Read more about sideload scoping here: www.graphiti.dev/concepts/relationships#customizing-scope
|
|
317
298
|
|
|
318
299
|
Here's the original, underlying error:
|
|
319
300
|
|
|
@@ -334,7 +315,7 @@ module Graphiti
|
|
|
334
315
|
#{@resource_class.name}: error occurred while sideloading "#{@sideload_name}"!
|
|
335
316
|
|
|
336
317
|
The error was raised while attempting to assign relevant model instances. Read
|
|
337
|
-
more about sideload assignment here: www.graphiti.dev/
|
|
318
|
+
more about sideload assignment here: www.graphiti.dev/concepts/relationships#customizing-assignment
|
|
338
319
|
|
|
339
320
|
A good way to debug is to put a debugger within the 'assign' block.
|
|
340
321
|
|
|
@@ -468,7 +449,7 @@ module Graphiti
|
|
|
468
449
|
|
|
469
450
|
self.validate_endpoints = false
|
|
470
451
|
|
|
471
|
-
See https://www.graphiti.dev/
|
|
452
|
+
See https://www.graphiti.dev/concepts/links for more information.
|
|
472
453
|
|
|
473
454
|
The current endpoints allowed for this resource are: #{@resource_class.endpoints.inspect}
|
|
474
455
|
MSG
|
data/lib/graphiti/query.rb
CHANGED
|
@@ -209,7 +209,7 @@ module Graphiti
|
|
|
209
209
|
allowlist = nil
|
|
210
210
|
if @resource.context&.respond_to?(:sideload_allowlist)
|
|
211
211
|
allowlist = @resource.context.sideload_allowlist
|
|
212
|
-
allowlist = allowlist[@resource.
|
|
212
|
+
allowlist = allowlist[@resource.current_action] if allowlist
|
|
213
213
|
end
|
|
214
214
|
|
|
215
215
|
scrubbed = allowlist ? Util::IncludeParams.scrub(requested, allowlist) : requested
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Graphiti
|
|
2
|
+
module Rails
|
|
3
|
+
# Wraps controller actions in a [Graphiti Context](https://www.graphiti.dev/concepts/resources#context) which points to the
|
|
4
|
+
# controller instance by default.
|
|
5
|
+
module Context
|
|
6
|
+
def self.included(klass)
|
|
7
|
+
klass.class_eval do
|
|
8
|
+
include Graphiti::Context
|
|
9
|
+
around_action :wrap_graphiti_context
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Called by [`#around_action`](https://api.rubyonrails.org/classes/AbstractController/Callbacks/ClassMethods.html#method-i-around_action)
|
|
14
|
+
# to wrap the current action in a Graphiti context defined by {#graphiti_context}.
|
|
15
|
+
def wrap_graphiti_context
|
|
16
|
+
Graphiti.with_context(graphiti_context, action_name.to_sym) do
|
|
17
|
+
yield
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# The context to use for Graphiti Resources. Defaults to the controller instance.
|
|
22
|
+
# Can be redefined for different behavior.
|
|
23
|
+
def graphiti_context
|
|
24
|
+
if respond_to?(:jsonapi_context)
|
|
25
|
+
DEPRECATOR.deprecation_warning("Overriding jsonapi_context", "Override #graphiti_context instead")
|
|
26
|
+
jsonapi_context
|
|
27
|
+
else
|
|
28
|
+
self
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module Graphiti
|
|
2
|
+
module Rails
|
|
3
|
+
# Everything a controller needs to serve Graphiti resources. Include it in
|
|
4
|
+
# the controller your API inherits from:
|
|
5
|
+
#
|
|
6
|
+
# class ApplicationController < ActionController::Base
|
|
7
|
+
# include Graphiti::Rails::Controller
|
|
8
|
+
# end
|
|
9
|
+
#
|
|
10
|
+
# Graphiti applied all of this to every controller in the application until
|
|
11
|
+
# 2.0, which meant wrapping unrelated actions in a Graphiti context and
|
|
12
|
+
# registering a catch-all exception handler on controllers that never touch
|
|
13
|
+
# Graphiti. Scoping it is now up to you.
|
|
14
|
+
module Controller
|
|
15
|
+
extend ActiveSupport::Concern
|
|
16
|
+
|
|
17
|
+
included do
|
|
18
|
+
include Graphiti::Rails::Context
|
|
19
|
+
include Graphiti::Rails::Debugging
|
|
20
|
+
|
|
21
|
+
# Broad, but the handler only renders formats listed in
|
|
22
|
+
# config.graphiti.handled_exception_formats and hands everything else
|
|
23
|
+
# back to Rails.
|
|
24
|
+
register_exception Exception,
|
|
25
|
+
status: :passthrough,
|
|
26
|
+
handler: Graphiti::Rails::FallbackHandler
|
|
27
|
+
|
|
28
|
+
register_exception Graphiti::Errors::InvalidRequest,
|
|
29
|
+
status: 400, handler: Graphiti::Rails::InvalidRequestHandler
|
|
30
|
+
register_exception Graphiti::Errors::ConflictRequest,
|
|
31
|
+
status: 409, handler: Graphiti::Rails::ConflictRequestHandler
|
|
32
|
+
register_exception Graphiti::Errors::RecordNotFound,
|
|
33
|
+
status: 404, handler: Graphiti::Rails::ExceptionHandler
|
|
34
|
+
register_exception Graphiti::Errors::RemoteWrite,
|
|
35
|
+
status: 400, handler: Graphiti::Rails::ExceptionHandler
|
|
36
|
+
register_exception Graphiti::Errors::SingularSideload,
|
|
37
|
+
status: 400, handler: Graphiti::Rails::ExceptionHandler
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Graphiti
|
|
2
|
+
module Rails
|
|
3
|
+
# Wraps controller actions in a [Graphiti Debugger](https://www.graphiti.dev/topics/debugging#debugger).
|
|
4
|
+
module Debugging
|
|
5
|
+
def self.included(klass)
|
|
6
|
+
klass.around_action :debug_graphiti
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# Called by [`#around_action`](https://api.rubyonrails.org/classes/AbstractController/Callbacks/ClassMethods.html#method-i-around_action)
|
|
10
|
+
# to wrap the current action in a Graphiti Debugger.
|
|
11
|
+
def debug_graphiti
|
|
12
|
+
Debugger.debug do
|
|
13
|
+
yield
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
module Graphiti
|
|
2
|
+
module Rails
|
|
3
|
+
class ExceptionHandler < RescueRegistry::ExceptionHandler
|
|
4
|
+
# We've actually changed the signature here which is somewhat risky...
|
|
5
|
+
def build_payload(show_details: false, traces: nil, style: :rails)
|
|
6
|
+
case style
|
|
7
|
+
when :standard
|
|
8
|
+
super(show_details: show_details, traces: traces).tap do |payload|
|
|
9
|
+
if show_details
|
|
10
|
+
# For Vandal and Request Responses
|
|
11
|
+
payload[:__raw_error__] = {
|
|
12
|
+
message: exception.message,
|
|
13
|
+
debug: exception.instance_variable_get(:@__graphiti_debug),
|
|
14
|
+
backtrace: exception.backtrace
|
|
15
|
+
}
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
when :rails
|
|
19
|
+
# TODO: Find way to not duplicate RailsExceptionHandler
|
|
20
|
+
body = {
|
|
21
|
+
status: status_code,
|
|
22
|
+
error: title
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if show_details
|
|
26
|
+
body[:exception] = exception.inspect
|
|
27
|
+
body[:traces] = traces if traces
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
body
|
|
31
|
+
else
|
|
32
|
+
raise ArgumentError, "unknown style #{style}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def formatted_response(content_type, **options)
|
|
37
|
+
# We're relying on the fact that `formatted_response` passes through unknown options to `build_payload`
|
|
38
|
+
if Graphiti::Rails.handled_exception_formats.include?(content_type.to_sym)
|
|
39
|
+
options[:style] = :standard
|
|
40
|
+
end
|
|
41
|
+
super
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class FallbackHandler < ExceptionHandler
|
|
46
|
+
# A nil response is what hands the exception back to the default Rails
|
|
47
|
+
# handler, so formats Graphiti does not claim render as they always did.
|
|
48
|
+
def formatted_response(content_type, **options)
|
|
49
|
+
return unless Graphiti::Rails.handled_exception_formats.include?(content_type.to_sym)
|
|
50
|
+
|
|
51
|
+
super
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
class InvalidRequestHandler < ExceptionHandler
|
|
56
|
+
# NOTE: `style` is ignored. A rejected request always reports which
|
|
57
|
+
# parameters were rejected and why.
|
|
58
|
+
def build_payload(show_details: false, traces: nil, style: nil)
|
|
59
|
+
{errors: error_serializer.new(exception.errors).errors}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def error_serializer
|
|
65
|
+
Graphiti::ErrorSerializers::InvalidRequest
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
class ConflictRequestHandler < InvalidRequestHandler
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def error_serializer
|
|
73
|
+
Graphiti::ErrorSerializers::ConflictRequest
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Graphiti
|
|
4
|
+
module Rails
|
|
5
|
+
# This Railtie exposes some configuration options:
|
|
6
|
+
# * `config.graphiti.handled_exception_formats`, defaulting to `[:jsonapi]`.
|
|
7
|
+
# Formats in this list will always have their exceptions handled by Graphiti.
|
|
8
|
+
# * `config.graphiti.respond_to_formats`, defaulting to `[:json, :jsonapi, :xml]`.
|
|
9
|
+
# When {Graphiti::Rails::Responders} is included in a controller it will respond
|
|
10
|
+
# to these mime types by default.
|
|
11
|
+
class Railtie < ::Rails::Railtie
|
|
12
|
+
config.graphiti = ActiveSupport::OrderedOptions.new
|
|
13
|
+
|
|
14
|
+
config.graphiti.handled_exception_formats = [:jsonapi]
|
|
15
|
+
|
|
16
|
+
old_respond_to_formats = Graphiti::DEPRECATOR.silence { Graphiti.config.try(:respond_to) }
|
|
17
|
+
config.graphiti.respond_to_formats = old_respond_to_formats || [:json, :jsonapi, :xml]
|
|
18
|
+
|
|
19
|
+
rake_tasks do
|
|
20
|
+
load File.expand_path("../../tasks/graphiti.rake", __dir__)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
initializer "graphiti-rails.config" do |app|
|
|
24
|
+
Graphiti::Rails.handled_exception_formats = app.config.graphiti.handled_exception_formats
|
|
25
|
+
Graphiti::Rails.respond_to_formats = app.config.graphiti.respond_to_formats
|
|
26
|
+
|
|
27
|
+
root = ::Rails.root
|
|
28
|
+
|
|
29
|
+
config_file = root.join(".graphiticfg.yml")
|
|
30
|
+
if config_file.exist?
|
|
31
|
+
cfg = YAML.load_file(config_file)
|
|
32
|
+
Graphiti.config.schema_path = root.join("public#{cfg["namespace"]}/schema.json")
|
|
33
|
+
else
|
|
34
|
+
Graphiti.config.schema_path = root.join("public/schema.json")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
initializer "graphti-rails.logger" do
|
|
39
|
+
config.after_initialize do
|
|
40
|
+
Graphiti.config.debug = ::Rails.logger.debug? && Graphiti.config.debug
|
|
41
|
+
Graphiti.logger = ::Rails.logger
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
initializer "graphiti-rails.init" do
|
|
46
|
+
if ::Rails.application.config.eager_load
|
|
47
|
+
config.after_initialize do |app|
|
|
48
|
+
::Rails.application.reload_routes!
|
|
49
|
+
Graphiti.setup!
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
register_mime_type
|
|
54
|
+
register_parameter_parser
|
|
55
|
+
register_renderers
|
|
56
|
+
establish_concurrency
|
|
57
|
+
configure_endpoint_lookup
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# from jsonapi-rails
|
|
61
|
+
MEDIA_TYPE = "application/vnd.api+json"
|
|
62
|
+
|
|
63
|
+
PARSER = lambda do |body|
|
|
64
|
+
data = JSON.parse(body)
|
|
65
|
+
data[:format] = :jsonapi
|
|
66
|
+
data.with_indifferent_access
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def register_mime_type
|
|
70
|
+
Mime::Type.register(MEDIA_TYPE, :jsonapi)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def register_parameter_parser
|
|
74
|
+
ActionDispatch::Request.parameter_parsers[:jsonapi] ||= PARSER
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def register_renderers
|
|
78
|
+
ActiveSupport.on_load(:action_controller) do
|
|
79
|
+
::ActionController::Renderers.add(:jsonapi) do |proxy, options|
|
|
80
|
+
self.content_type ||= Mime[:jsonapi]
|
|
81
|
+
|
|
82
|
+
# opts = {}
|
|
83
|
+
# if respond_to?(:default_jsonapi_render_options)
|
|
84
|
+
# opts = default_jsonapi_render_options
|
|
85
|
+
# end
|
|
86
|
+
|
|
87
|
+
if proxy.is_a?(Hash) # for destroy
|
|
88
|
+
render(options.merge(json: proxy))
|
|
89
|
+
else
|
|
90
|
+
proxy.to_jsonapi(options)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
ActiveSupport.on_load(:action_controller) do
|
|
96
|
+
ActionController::Renderers.add(:jsonapi_errors) do |proxy, options|
|
|
97
|
+
self.content_type ||= Mime[:jsonapi]
|
|
98
|
+
|
|
99
|
+
validation = Graphiti::ErrorSerializers::Validation.new \
|
|
100
|
+
proxy.data, proxy.payload.relationships
|
|
101
|
+
|
|
102
|
+
render \
|
|
103
|
+
json: {errors: validation.errors},
|
|
104
|
+
status: :unprocessable_entity
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Only run concurrently if our environment supports it
|
|
110
|
+
def establish_concurrency
|
|
111
|
+
Graphiti.config.concurrency = !::Rails.env.test? &&
|
|
112
|
+
::Rails.application.config.cache_classes
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def configure_endpoint_lookup
|
|
116
|
+
Graphiti.config.context_for_endpoint = ->(path, action) {
|
|
117
|
+
method = :GET
|
|
118
|
+
case action
|
|
119
|
+
when :show then path = "#{path}/1"
|
|
120
|
+
when :create then method = :POST
|
|
121
|
+
when :update
|
|
122
|
+
path = "#{path}/1"
|
|
123
|
+
method = :PUT
|
|
124
|
+
when :destroy
|
|
125
|
+
path = "#{path}/1"
|
|
126
|
+
method = :DELETE
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
route = begin
|
|
130
|
+
::Rails.application.routes.recognize_path(path.to_s, method: method)
|
|
131
|
+
rescue
|
|
132
|
+
nil
|
|
133
|
+
end
|
|
134
|
+
"#{route[:controller]}_controller".classify.safe_constantize if route
|
|
135
|
+
}
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# For use with the `responders` gem responders gem to get respond_with
|
|
2
|
+
module Graphiti
|
|
3
|
+
module Rails
|
|
4
|
+
module Responders
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
include ActionController::MimeResponds
|
|
9
|
+
respond_to(*Graphiti::Rails.respond_to_formats)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Override to avoid location url generation (for now)
|
|
13
|
+
def respond_with(*args, &blk)
|
|
14
|
+
opts = args.extract_options!
|
|
15
|
+
opts[:location] = nil
|
|
16
|
+
args << opts
|
|
17
|
+
super(*args, &blk)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Graphiti
|
|
2
|
+
module Rails
|
|
3
|
+
# Wrap a request in `handle_request_exceptions` to assert on the rendered
|
|
4
|
+
# error payload. Exceptions propagate untouched in tests otherwise.
|
|
5
|
+
module TestHelpers
|
|
6
|
+
include RescueRegistry::RailsTestHelpers
|
|
7
|
+
|
|
8
|
+
# rescue_registry sets action_dispatch.show_exceptions to true/false.
|
|
9
|
+
# Rails 7.1 replaced those with :all/:rescuable/:none, and Rails 8 dropped
|
|
10
|
+
# boolean handling altogether. ExceptionWrapper#show? now treats
|
|
11
|
+
# everything that is not :none as "show", so passing false would leave
|
|
12
|
+
# exception handling on and silently do nothing.
|
|
13
|
+
def handle_request_exceptions(handle = true, &block)
|
|
14
|
+
super(handle ? :all : :none, &block)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def handle_request_exceptions?
|
|
18
|
+
::Rails.application.config.action_dispatch.show_exceptions != :none
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|