graphiti 2.0.0.beta.11 → 2.0.0.beta.13

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +6 -0
  3. data/CHANGELOG.md +39 -0
  4. data/Gemfile +3 -0
  5. data/lib/generators/graphiti/generator_mixin.rb +5 -4
  6. data/lib/generators/graphiti/install_generator.rb +15 -31
  7. data/lib/generators/graphiti/locale_generator.rb +86 -0
  8. data/lib/generators/graphiti/resource_generator.rb +2 -1
  9. data/lib/generators/graphiti/templates/application_resource.rb.erb +6 -9
  10. data/lib/generators/graphiti/templates/controller.rb.erb +1 -1
  11. data/lib/generators/graphiti/templates/locale.yml.erb +30 -0
  12. data/lib/generators/graphiti/templates/resource_writes_spec.rb.erb +1 -1
  13. data/lib/graphiti/adapters/active_record.rb +1 -1
  14. data/lib/graphiti/context_hash.rb +21 -0
  15. data/lib/graphiti/debugger.rb +3 -3
  16. data/lib/graphiti/error_serializers/conflict_request.rb +1 -1
  17. data/lib/graphiti/error_serializers/deprecated_constants.rb +6 -16
  18. data/lib/graphiti/error_serializers/invalid_request.rb +3 -1
  19. data/lib/graphiti/error_serializers/translated_title.rb +13 -0
  20. data/lib/graphiti/error_serializers/validation.rb +14 -4
  21. data/lib/graphiti/query.rb +8 -1
  22. data/lib/graphiti/rails/exception_handlers.rb +17 -0
  23. data/lib/graphiti/rails/railtie.rb +11 -1
  24. data/lib/graphiti/rails/rake_helpers.rb +5 -0
  25. data/lib/graphiti/rails.rb +18 -0
  26. data/lib/graphiti/request_validators/update_validator.rb +2 -6
  27. data/lib/graphiti/request_validators/validator.rb +6 -6
  28. data/lib/graphiti/resource.rb +3 -3
  29. data/lib/graphiti/resource_proxy.rb +14 -10
  30. data/lib/graphiti/schema/check.rb +69 -0
  31. data/lib/graphiti/schema.rb +8 -9
  32. data/lib/graphiti/scope.rb +4 -5
  33. data/lib/graphiti/sideload.rb +46 -9
  34. data/lib/graphiti/spec_helpers/matchers.rb +2 -1
  35. data/lib/graphiti/spec_helpers/rspec.rb +5 -13
  36. data/lib/graphiti/util/serializer_relationships.rb +1 -10
  37. data/lib/graphiti/util/simple_errors.rb +26 -5
  38. data/lib/graphiti/util/transaction_hooks_recorder.rb +2 -2
  39. data/lib/graphiti/version.rb +1 -1
  40. data/lib/graphiti.rb +9 -4
  41. data/lib/tasks/graphiti.rake +18 -0
  42. metadata +6 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a639e421891e5d5e650d2d8131a75905cb9ffd0fb40cdfd15239ca5caf29d12f
4
- data.tar.gz: 95172bbff224d598c959d5acc93c30861e01c50540fdea871ea5029473bf5259
3
+ metadata.gz: 2963ad9851cf238b3885921c2b4858bf3b7aefe1fad5726a588c966da8cd0ada
4
+ data.tar.gz: 6486ac49fff4aa22faaa46cc2aef5439a015943748fd323288c8f90f596fbf9a
5
5
  SHA512:
6
- metadata.gz: 24c275cfcaecdba9ced126b7970b37962998673a3a8609c885a69befc53adba646ec2c7d9517198509e358ebba5300f3b336c86f02df98af9d18a6f4a4c306c3
7
- data.tar.gz: e7d2fd4e0d0ff74b68ea4d3673399d1b72897ad7c8647fa38835ff2b58c850d58ba95a1145a52f55893e88e36eb0aa90bece72a2e6e593173661d16100666edc
6
+ metadata.gz: b25b468d3ddf0efc6aa513940ae06c2cc30150e7d4a743bd5ba1d4a56a768f3a7452780b515bbd68610ab51bdd979d577f7644d83a0fa724e975a45a547e1bef
7
+ data.tar.gz: 02a6b4f82e8e7c2d9582c8765d49ef07901e001ea942846d3b70f79ea25c7e83d4d032c5644404420c075e0565eb946cfb1d54356ed319aefb2841e644bb0944
@@ -85,6 +85,12 @@ jobs:
85
85
  bundler-cache: true
86
86
  - name: Run tests
87
87
  run: bundle exec rspec
88
+ # spec_helper narrows "bundle exec rspec" to spec/integration/rails when
89
+ # APPRAISAL_INITIALIZED is set, so the step above never runs the library
90
+ # suite. This is the only cell that runs it against a pinned Rails.
91
+ - name: Run the library suite against this Rails
92
+ if: matrix.appraisal
93
+ run: env -u APPRAISAL_INITIALIZED bundle exec rspec
88
94
  example-app:
89
95
  name: "Example app (employee_directory)"
90
96
  runs-on: ubuntu-latest
data/CHANGELOG.md CHANGED
@@ -1,5 +1,44 @@
1
1
  graphiti changelog
2
2
 
3
+ # [2.0.0-beta.13](https://github.com/graphiti-api/graphiti/compare/v2.0.0-beta.12...v2.0.0-beta.13) (2026-08-31)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * compare the endpoint id to the payload id as strings ([62051d7](https://github.com/graphiti-api/graphiti/commit/62051d7531432357db7bc464cce77ccc55168d07))
9
+ * evaluate writable guards under the action being performed ([d56b86a](https://github.com/graphiti-api/graphiti/commit/d56b86a5ffaac4106aaea1e2ee5400069a4205fe))
10
+ * **generators:** stop injecting a routes host into config/application.rb ([db92d06](https://github.com/graphiti-api/graphiti/commit/db92d0649ca0246662a86b1172229eaa2a817d2b))
11
+ * keep a belongs_to linkage when another include shares its name ([2c9c7c7](https://github.com/graphiti-api/graphiti/commit/2c9c7c7ac06cd319ac95e1445eb55a0322841de2))
12
+ * keep deep filters narrow when an include path repeats ([833a10b](https://github.com/graphiti-api/graphiti/commit/833a10be120a4f2cd615ad05f750c7f246d62801))
13
+ * keep request state in fiber storage so it survives into child fibers ([6569935](https://github.com/graphiti-api/graphiti/commit/6569935c2140d4dd91cd6d4a7f9ec7c1bd05c888))
14
+ * only register the jsonapi mime type when it is missing ([88401ab](https://github.com/graphiti-api/graphiti/commit/88401abc8c77f1e859b4b8da3f9574c75834eb43))
15
+ * with_options accepts the predicate backed relationship options ([71ab441](https://github.com/graphiti-api/graphiti/commit/71ab4418bccc4cff7e6cc10151c1728641d25d34))
16
+
17
+
18
+ ### Features
19
+
20
+ * write every error code Graphiti renders into the generated locale file ([15f19c1](https://github.com/graphiti-api/graphiti/commit/15f19c1dbe56559d9d041fa50e2cc5ea7b2dcf4b))
21
+
22
+
23
+ ### Performance Improvements
24
+
25
+ * record latest performance stats ([7380ef9](https://github.com/graphiti-api/graphiti/commit/7380ef912e9f3b99618bf47ed04e06d1efad4e6c))
26
+
27
+ # [2.0.0-beta.12](https://github.com/graphiti-api/graphiti/compare/v2.0.0-beta.11...v2.0.0-beta.12) (2026-08-30)
28
+
29
+
30
+ ### Bug Fixes
31
+
32
+ * compare each concurrency path against itself in the table to better illustrate the performance wins ([d0f0e92](https://github.com/graphiti-api/graphiti/commit/d0f0e92860b3b4affe3fca6dbb9b871fe8a1374f))
33
+
34
+
35
+ ### Features
36
+
37
+ * add schema rake tasks, so checking a schema no longer means running the suite ([6f4783d](https://github.com/graphiti-api/graphiti/commit/6f4783d5654659125b7eb358bf1175b6feaacb7c))
38
+ * declare Graphiti's client errors in rescue_responses ([e369824](https://github.com/graphiti-api/graphiti/commit/e369824d6e11b671ffd212caad3d0a43a7ceefcf))
39
+ * mark deprecated settings in the generated ApplicationResource ([52b0642](https://github.com/graphiti-api/graphiti/commit/52b0642e689a16d14dacaf4e28a2641f10774cf0))
40
+ * take error text from locale keys ([d01fd06](https://github.com/graphiti-api/graphiti/commit/d01fd06fc1ede8c3dd798ec48de4d5b01645d87e)), closes [#216](https://github.com/graphiti-api/graphiti/issues/216)
41
+
3
42
  # [2.0.0-beta.11](https://github.com/graphiti-api/graphiti/compare/v2.0.0-beta.10...v2.0.0-beta.11) (2026-08-27)
4
43
 
5
44
 
data/Gemfile CHANGED
@@ -3,6 +3,9 @@ source "https://rubygems.org"
3
3
  # Specify your gem's dependencies in graphiti.gemspec
4
4
  gemspec
5
5
 
6
+ # Until rescue_registry 1.1 ships with the fiber storage fix.
7
+ gem "rescue_registry", github: "wagenet/rescue_registry", branch: "develop"
8
+
6
9
  group :test do
7
10
  gem "database_cleaner"
8
11
  gem "pry"
@@ -50,11 +50,12 @@ module Graphiti
50
50
  end
51
51
 
52
52
  def resource_setting_hint(setting)
53
- return setting[:note] if setting[:note]
54
- return unless setting[:values]
53
+ hint = setting[:note] || setting[:values]&.map(&:inspect)
54
+ &.to_sentence(two_words_connector: " or ", last_word_connector: ", or ")
55
55
 
56
- setting[:values].map(&:inspect)
57
- .to_sentence(two_words_connector: " or ", last_word_connector: ", or ")
56
+ return hint unless setting[:deprecated]
57
+
58
+ ["deprecated, removed in 3.0", hint].compact.join(". ")
58
59
  end
59
60
 
60
61
  def graphiti_config
@@ -1,4 +1,5 @@
1
1
  require_relative "generator_mixin"
2
+ require_relative "locale_generator"
2
3
 
3
4
  module Graphiti
4
5
  class InstallGenerator < ::Rails::Generators::Base
@@ -17,6 +18,8 @@ module Graphiti
17
18
  to = File.join("app/resources", "application_resource.rb")
18
19
  template("application_resource.rb.erb", to)
19
20
 
21
+ invoke(Graphiti::LocaleGenerator, [], options)
22
+
20
23
  inject_into_file "app/controllers/application_controller.rb", after: "class ApplicationController < ActionController::API\n" do
21
24
  app_controller_code
22
25
  end
@@ -25,23 +28,8 @@ module Graphiti
25
28
  app_controller_code
26
29
  end
27
30
 
28
- inject_into_file "config/application.rb", after: "Rails::Application\n" do
29
- <<-'RUBY'
30
- # In order for Graphiti to generate links, you need to set the routes host.
31
- # When not explicitly set, via the HOST env var, this will fall back to
32
- # the rails server settings.
33
- # Rails::Server is not defined in console or rake tasks, so this will only
34
- # use those defaults when they are available.
35
- routes.default_url_options[:host] = ENV.fetch('HOST') do
36
- if defined?(Rails::Server)
37
- argv_options = Rails::Server::Options.new.parse!(ARGV)
38
- "http://#{argv_options[:Host]}:#{argv_options[:Port]}"
39
- end
40
- end
41
- RUBY
42
- end
43
-
44
- if defined?(RSpec)
31
+ # Thor aborts the whole generator when this file is missing.
32
+ if defined?(RSpec) && File.exist?(File.join(destination_root, "spec/rails_helper.rb"))
45
33
  inject_into_file "spec/rails_helper.rb", after: /RSpec.configure.+^end$/m do
46
34
  <<~RUBY
47
35
 
@@ -57,20 +45,7 @@ module Graphiti
57
45
  end
58
46
 
59
47
  insert_into_file "config/routes.rb", after: "Rails.application.routes.draw do\n" do
60
- if defined?(VandalUi)
61
- <<-STR
62
- scope path: ApplicationResource.endpoint_namespace, defaults: { format: :jsonapi } do
63
- mount VandalUi::Engine, at: '/vandal'
64
- # your routes go here
65
- end
66
- STR
67
- else
68
- <<-STR
69
- scope path: ApplicationResource.endpoint_namespace, defaults: { format: :jsonapi } do
70
- # your routes go here
71
- end
72
- STR
73
- end
48
+ namespace_scope
74
49
  end
75
50
  end
76
51
 
@@ -80,6 +55,15 @@ module Graphiti
80
55
  @options["omit-comments"]
81
56
  end
82
57
 
58
+ # The resource generator re-finds this scope by its format default.
59
+ def namespace_scope
60
+ lines = [" scope path: \"#{api_namespace}\", defaults: {format: :jsonapi} do\n"]
61
+ lines << " mount VandalUi::Engine, at: '/vandal'\n" if defined?(VandalUi)
62
+ lines << " # your routes go here\n"
63
+ lines << " end\n"
64
+ lines.join
65
+ end
66
+
83
67
  def app_controller_code
84
68
  str = +" include Graphiti::Rails::Controller\n"
85
69
  if defined?(::Responders)
@@ -0,0 +1,86 @@
1
+ module Graphiti
2
+ class LocaleGenerator < ::Rails::Generators::Base
3
+ source_root File.expand_path("templates", __dir__)
4
+
5
+ class_option :"omit-comments",
6
+ type: :boolean,
7
+ default: false,
8
+ aliases: ["-c"],
9
+ desc: "Generate without documentation comments"
10
+
11
+ # Every code Graphiti renders a title for, and the title it renders today.
12
+ ERROR_TITLES = {
13
+ bad_request: "Request Error",
14
+ not_found: "Not Found",
15
+ conflict: "Conflict Error",
16
+ unprocessable_entity: "Validation Error",
17
+ internal_server_error: "Internal Server Error"
18
+ }.freeze
19
+
20
+ # The two codes no registration produces.
21
+ ERROR_SOURCES = {
22
+ unprocessable_entity: ["a write that failed model validations"],
23
+ internal_server_error: ["any exception Graphiti did not register"]
24
+ }.freeze
25
+
26
+ # Written into the app's locale file, not defaulted in Graphiti.
27
+ ERROR_DETAILS = {
28
+ internal_server_error: "We've probably received an error report already, but please contact us if the issue persists."
29
+ }.freeze
30
+
31
+ COMMENT_WIDTH = 68
32
+
33
+ desc "Writes the error text Graphiti renders, ready to edit"
34
+ def locale
35
+ template("locale.yml.erb", File.join("config/locales", "graphiti.en.yml"))
36
+ end
37
+
38
+ private
39
+
40
+ def omit_comments?
41
+ @options["omit-comments"]
42
+ end
43
+
44
+ def error_titles
45
+ ERROR_TITLES
46
+ end
47
+
48
+ def error_detail(code)
49
+ ERROR_DETAILS[code]
50
+ end
51
+
52
+ # What raises this code, so the file says where its text is used.
53
+ def error_source_lines(code)
54
+ ERROR_SOURCES[code] || wrap(exception_names(code))
55
+ end
56
+
57
+ def exception_names(code)
58
+ Graphiti::Rails::CLIENT_ERROR_STATUSES
59
+ .select { |_exception, status| status == code }
60
+ .keys.map { |exception| exception.delete_prefix("Graphiti::Errors::") }
61
+ end
62
+
63
+ def wrap(names)
64
+ items = names.map.with_index do |name, index|
65
+ (index == names.length - 1) ? name : "#{name},"
66
+ end
67
+
68
+ items.each_with_object([]) do |item, lines|
69
+ joined = "#{lines.last} #{item}"
70
+ if lines.empty? || joined.length > COMMENT_WIDTH
71
+ lines << item
72
+ else
73
+ lines[-1] = joined
74
+ end
75
+ end
76
+ end
77
+
78
+ def default_messages
79
+ Graphiti::Util::SimpleErrors::DEFAULT_MESSAGES
80
+ end
81
+
82
+ def default_format
83
+ Graphiti::Util::SimpleErrors::DEFAULT_FORMAT
84
+ end
85
+ end
86
+ end
@@ -171,7 +171,8 @@ module Graphiti
171
171
  code << %(, controller: "#{controller_path}") if options[:controller]
172
172
  code << %(, only: [#{actions.map { |a| ":#{a}" }.join(", ")}]) if actions.length < 5
173
173
  code << "\n"
174
- inject_into_file "config/routes.rb", after: /ApplicationResource.*$\n/ do
174
+ # Matches the install generator's scope, including its pre-2.0 form.
175
+ inject_into_file "config/routes.rb", after: /format: :jsonapi.*$\n/ do
175
176
  indent(code, 4)
176
177
  end
177
178
  end
@@ -1,21 +1,18 @@
1
1
  <%- unless omit_comments? -%>
2
- # ApplicationResource is similar to ApplicationRecord - a base class that
3
- # holds configuration/methods for subclasses.
4
2
  # All Resources should inherit from ApplicationResource.
5
3
  <%- end -%>
6
4
  class ApplicationResource < Graphiti::Resource
7
- <%- unless omit_comments? -%>
8
- # Use the ActiveRecord Adapter for all subclasses.
9
- # Subclasses can still override this default.
10
- <%- end -%>
11
5
  self.abstract_class = true
12
6
  self.adapter = Graphiti::Adapters::ActiveRecord
13
- self.base_url = Rails.application.routes.default_url_options[:host]
7
+ <%- unless omit_comments? -%>
8
+ # Or follow config/environments:
9
+ # self.base_url = ActionDispatch::Http::URL.url_for(Rails.application.routes.default_url_options)
10
+ <%- end -%>
11
+ self.base_url = ENV.fetch('BASE_URL', 'http://localhost:3000')
14
12
  self.endpoint_namespace = '<%= api_namespace %>'
15
13
  <%- unless omit_comments? -%>
16
14
 
17
- # Defaults every Resource inherits. Uncomment to change them API-wide, or set
18
- # them on individual Resources. See https://graphiti.dev/concepts/resources
15
+ # Defaults every Resource inherits. See https://graphiti.dev/concepts/resources
19
16
  <%- resource_setting_groups.each_pair do |group, lines| -%>
20
17
 
21
18
  # <%= group.to_s.humanize %>
@@ -56,7 +56,7 @@ class <%= controller_class_name %> < ApplicationController
56
56
  def update
57
57
  <%= file_name %> = <%= resource_klass %>.find(params)
58
58
 
59
- if <%= file_name %>.update_attributes
59
+ if <%= file_name %>.update
60
60
  render jsonapi: <%= file_name %>
61
61
  else
62
62
  render jsonapi_errors: <%= file_name %>
@@ -0,0 +1,30 @@
1
+ <%- unless omit_comments? -%>
2
+ # For another language, add a block below or a graphiti.<locale>.yml file.
3
+ <%- end -%>
4
+ en:
5
+ graphiti:
6
+ errors:
7
+ <%- unless omit_comments? -%>
8
+ # Keyed by error code. Edit any value to reword it. Comments name what
9
+ # raises each code, relative to Graphiti::Errors.
10
+ <%- end -%>
11
+ <%- error_titles.each_pair do |code, title| -%>
12
+ <%= code %>:
13
+ <%- unless omit_comments? -%>
14
+ <%- error_source_lines(code).each do |line| -%>
15
+ # <%= line %>
16
+ <%- end -%>
17
+ <%- end -%>
18
+ title: <%= title.inspect %>
19
+ <%- if error_detail(code) -%>
20
+ detail: <%= error_detail(code).inspect %>
21
+ <%- end -%>
22
+ <%- end -%>
23
+ <%- unless omit_comments? -%>
24
+ # format joins the two below: "data.attributes.title is an unknown attribute".
25
+ <%- end -%>
26
+ format: <%= default_format.inspect %>
27
+ messages:
28
+ <%- default_messages.each do |code, message| -%>
29
+ <%= code %>: <%= message.inspect %>
30
+ <%- end -%>
@@ -45,7 +45,7 @@ RSpec.describe <%= resource_class %>, type: :resource do
45
45
 
46
46
  xit 'works (add some attributes and enable this spec)' do
47
47
  expect {
48
- expect(instance.update_attributes).to eq(true)
48
+ expect(instance.update).to eq(true)
49
49
  }.to change { <%= var %>.reload.updated_at }
50
50
  # .and change { <%= var %>.foo }.to('bar') <- example
51
51
  end
@@ -246,7 +246,7 @@ module Graphiti
246
246
 
247
247
  children.each do |child|
248
248
  if association_type == :many_to_many &&
249
- [:create, :update].include?(Graphiti.context[:namespace]) &&
249
+ [:create, :update].include?(Graphiti.context[:action]) &&
250
250
  !parent.send(association_name).exists?(child.id) &&
251
251
  child.errors.blank?
252
252
  parent.send(association_name) << child
@@ -0,0 +1,21 @@
1
+ module Graphiti
2
+ # graphiti-api/graphiti#134 recommended reading Graphiti.context[:namespace] from base_scope before current_action existed, so the old key keeps working for one major.
3
+ class ContextHash < Hash
4
+ def [](key)
5
+ super(deprecate_namespace(key))
6
+ end
7
+
8
+ def []=(key, value)
9
+ super(deprecate_namespace(key), value)
10
+ end
11
+
12
+ private
13
+
14
+ def deprecate_namespace(key)
15
+ return key unless key == :namespace
16
+
17
+ Graphiti::DEPRECATOR.deprecation_warning(:"context[:namespace]", "Use #current_action instead", caller_locations(2))
18
+ :action
19
+ end
20
+ end
21
+ end
@@ -11,13 +11,13 @@ module Graphiti
11
11
  private_constant :CHUNKS
12
12
 
13
13
  class << self
14
- # Pool threads inherit this through the thread-local copy Scope#future_with_context makes, so their chunks reach the right request.
14
+ # Pool threads inherit this through the fiber storage copy Scope#future_with_context makes, so their chunks reach the right request.
15
15
  def chunks
16
- Thread.current[CHUNKS] ||= Concurrent::Array.new
16
+ Fiber[CHUNKS] ||= Concurrent::Array.new
17
17
  end
18
18
 
19
19
  def chunks=(value)
20
- Thread.current[CHUNKS] = value
20
+ Fiber[CHUNKS] = value
21
21
  end
22
22
 
23
23
  def on_data(name, start, stop, id, payload)
@@ -11,7 +11,7 @@ module Graphiti
11
11
  "conflict"
12
12
  end
13
13
 
14
- def title
14
+ def default_title
15
15
  "Conflict Error"
16
16
  end
17
17
  end
@@ -1,25 +1,15 @@
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.
1
+ # Only graphiti_errors' serializers survived the 2.0 merge. Remove in 3.0.
7
2
  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.
3
+ # Including an empty module succeeds, so silence here means HTML error pages.
12
4
  def self.included(klass)
13
5
  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:
6
+ GraphitiErrors merged into graphiti in 2.0 and this include does nothing. Use:
18
7
 
19
8
  include Graphiti::Rails::Controller
20
9
 
21
- which registers Graphiti's own handlers. Your own go alongside them with
22
- `register_exception`, available on every controller.
10
+ Your own exceptions register alongside Graphiti's with `register_exception`.
11
+ `registered_exception?` is now `RescueRegistry.handles_exception?`, and
12
+ `handle_exception` went with the rendering. See graphiti.dev/upgrading.
23
13
  MSG
24
14
  end
25
15
 
@@ -1,6 +1,8 @@
1
1
  module Graphiti
2
2
  module ErrorSerializers
3
3
  class InvalidRequest
4
+ include TranslatedTitle
5
+
4
6
  STATUS = 400
5
7
 
6
8
  attr_reader :source
@@ -48,7 +50,7 @@ module Graphiti
48
50
  "bad_request"
49
51
  end
50
52
 
51
- def title
53
+ def default_title
52
54
  "Request Error"
53
55
  end
54
56
 
@@ -0,0 +1,13 @@
1
+ module Graphiti
2
+ module ErrorSerializers
3
+ module TranslatedTitle
4
+ private
5
+
6
+ def title
7
+ return default_title unless defined?(::I18n)
8
+
9
+ ::I18n.t :title, scope: [:graphiti, :errors, code], default: default_title
10
+ end
11
+ end
12
+ end
13
+ end
@@ -3,6 +3,8 @@ module Graphiti
3
3
  # Walks into related objects so nested writes report against the record
4
4
  # that actually failed.
5
5
  class Validation
6
+ include TranslatedTitle
7
+
6
8
  attr_reader :object
7
9
 
8
10
  def initialize(object, relationship_payloads = {}, relationship_meta = {})
@@ -13,14 +15,14 @@ module Graphiti
13
15
 
14
16
  def attribute_errors
15
17
  [].tap do |errors|
16
- each_error do |attribute, message, code|
18
+ each_error do |attribute, message, validation_code|
17
19
  errors << {
18
- code: "unprocessable_entity",
20
+ code: code,
19
21
  status: "422",
20
- title: "Validation Error",
22
+ title: title,
21
23
  detail: detail_for(attribute, message),
22
24
  source: {pointer: pointer_for(object, attribute)},
23
- meta: meta_for(attribute, message, code, @relationship_meta)
25
+ meta: meta_for(attribute, message, validation_code, @relationship_meta)
24
26
  }
25
27
  end
26
28
  end
@@ -34,6 +36,14 @@ module Graphiti
34
36
 
35
37
  private
36
38
 
39
+ def code
40
+ "unprocessable_entity"
41
+ end
42
+
43
+ def default_title
44
+ "Validation Error"
45
+ end
46
+
37
47
  def each_error
38
48
  object.errors.messages.each_pair do |attribute, messages|
39
49
  details = object.errors.details.find { |k, _| k == attribute }[1]
@@ -26,6 +26,7 @@ module Graphiti
26
26
  @parents = parents
27
27
  @action = parse_action(action)
28
28
  @entity_map = Concurrent::Map.new if parents.empty?
29
+ @association_owners = Concurrent::Map.new if parents.empty?
29
30
  end
30
31
 
31
32
  def association?
@@ -39,6 +40,12 @@ module Graphiti
39
40
  @entity_map
40
41
  end
41
42
 
43
+ def association_owners
44
+ return root.association_owners unless root == self
45
+
46
+ @association_owners
47
+ end
48
+
42
49
  # Deduplication exists for a row two include paths both resolve, so a
43
50
  # resource class sitting at one node has nothing to collide with.
44
51
  def repeated_resource_classes
@@ -421,7 +428,7 @@ module Graphiti
421
428
  end
422
429
 
423
430
  def parse_action(action)
424
- action ||= @params.fetch(:action, Graphiti.context[:namespace]).try(:to_sym)
431
+ action ||= @params.fetch(:action, Graphiti.context[:action]).try(:to_sym)
425
432
  case action
426
433
  when :index
427
434
  :all
@@ -40,6 +40,23 @@ module Graphiti
40
40
  end
41
41
  super
42
42
  end
43
+
44
+ private
45
+
46
+ def title_from_status
47
+ translate(:title) || super
48
+ end
49
+
50
+ # rescue_registry's default tells the client its engineers have been notified.
51
+ def default_detail_for_status
52
+ translate(:detail)
53
+ end
54
+
55
+ def translate(key)
56
+ return unless defined?(::I18n)
57
+
58
+ ::I18n.t key, scope: [:graphiti, :errors, error_code], default: nil
59
+ end
43
60
  end
44
61
 
45
62
  class FallbackHandler < ExceptionHandler
@@ -20,6 +20,16 @@ module Graphiti
20
20
  load File.expand_path("../../tasks/graphiti.rake", __dir__)
21
21
  end
22
22
 
23
+ # ActionDispatch has already copied config.action_dispatch.rescue_responses into the wrapper by now.
24
+ initializer "graphiti-rails.rescue_responses", after: "action_dispatch.configure" do
25
+ responses = ActionDispatch::ExceptionWrapper.rescue_responses
26
+
27
+ Graphiti::Rails::CLIENT_ERROR_STATUSES.each do |exception, status|
28
+ # rescue_responses answers :internal_server_error for a missing key, so ||= never assigns.
29
+ responses[exception] = status unless responses.key?(exception)
30
+ end
31
+ end
32
+
23
33
  initializer "graphiti-rails.config" do |app|
24
34
  Graphiti::Rails.handled_exception_formats = app.config.graphiti.handled_exception_formats
25
35
  Graphiti::Rails.respond_to_formats = app.config.graphiti.respond_to_formats
@@ -67,7 +77,7 @@ module Graphiti
67
77
  end
68
78
 
69
79
  def register_mime_type
70
- Mime::Type.register(MEDIA_TYPE, :jsonapi)
80
+ Mime::Type.register(MEDIA_TYPE, :jsonapi) if Mime[:jsonapi].nil?
71
81
  end
72
82
 
73
83
  def register_parameter_parser
@@ -15,6 +15,11 @@ module Graphiti
15
15
  @session ||= ActionDispatch::Integration::Session.new(::Rails.application)
16
16
  end
17
17
 
18
+ # An engine runs the task from its own directory, not the host application's root.
19
+ def schema_path(path = nil)
20
+ path.nil? ? Graphiti.config.schema_path : File.expand_path(path, Dir.pwd)
21
+ end
22
+
18
23
  def setup_rails!
19
24
  ::Rails.application.eager_load!
20
25
  ::Rails.application.config.cache_classes = true
@@ -38,6 +38,24 @@ module Graphiti
38
38
  DEPRECATOR
39
39
  )
40
40
 
41
+ # Rails reports an exception to the error reporter unless its rescue_responses names it.
42
+ CLIENT_ERROR_STATUSES = {
43
+ "Graphiti::Errors::InvalidRequest" => :bad_request,
44
+ "Graphiti::Errors::RemoteWrite" => :bad_request,
45
+ "Graphiti::Errors::SingularSideload" => :bad_request,
46
+ "Graphiti::Errors::InvalidInclude" => :bad_request,
47
+ "Graphiti::Errors::UnsupportedSort" => :bad_request,
48
+ "Graphiti::Errors::UnsupportedOperator" => :bad_request,
49
+ "Graphiti::Errors::InvalidFilterValue" => :bad_request,
50
+ "Graphiti::Errors::RequiredFilter" => :bad_request,
51
+ "Graphiti::Errors::UnsupportedPageSize" => :bad_request,
52
+ "Graphiti::Errors::UnsupportedBeforeCursor" => :bad_request,
53
+ "Graphiti::Errors::UnsupportedPagination" => :bad_request,
54
+ "Graphiti::Errors::FilterGroupMissingRequiredFilters" => :bad_request,
55
+ "Graphiti::Errors::RecordNotFound" => :not_found,
56
+ "Graphiti::Errors::ConflictRequest" => :conflict
57
+ }.freeze
58
+
41
59
  # @!attribute self.handled_exception_formats
42
60
  # A list of formats as symbols whose exceptions will be handled by Graphiti. See {Railtie}.
43
61
  cattr_accessor :handled_exception_formats, default: []