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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +6 -0
- data/CHANGELOG.md +39 -0
- data/Gemfile +3 -0
- data/lib/generators/graphiti/generator_mixin.rb +5 -4
- data/lib/generators/graphiti/install_generator.rb +15 -31
- data/lib/generators/graphiti/locale_generator.rb +86 -0
- data/lib/generators/graphiti/resource_generator.rb +2 -1
- data/lib/generators/graphiti/templates/application_resource.rb.erb +6 -9
- data/lib/generators/graphiti/templates/controller.rb.erb +1 -1
- data/lib/generators/graphiti/templates/locale.yml.erb +30 -0
- data/lib/generators/graphiti/templates/resource_writes_spec.rb.erb +1 -1
- data/lib/graphiti/adapters/active_record.rb +1 -1
- data/lib/graphiti/context_hash.rb +21 -0
- data/lib/graphiti/debugger.rb +3 -3
- data/lib/graphiti/error_serializers/conflict_request.rb +1 -1
- data/lib/graphiti/error_serializers/deprecated_constants.rb +6 -16
- data/lib/graphiti/error_serializers/invalid_request.rb +3 -1
- data/lib/graphiti/error_serializers/translated_title.rb +13 -0
- data/lib/graphiti/error_serializers/validation.rb +14 -4
- data/lib/graphiti/query.rb +8 -1
- data/lib/graphiti/rails/exception_handlers.rb +17 -0
- data/lib/graphiti/rails/railtie.rb +11 -1
- data/lib/graphiti/rails/rake_helpers.rb +5 -0
- data/lib/graphiti/rails.rb +18 -0
- data/lib/graphiti/request_validators/update_validator.rb +2 -6
- data/lib/graphiti/request_validators/validator.rb +6 -6
- data/lib/graphiti/resource.rb +3 -3
- data/lib/graphiti/resource_proxy.rb +14 -10
- data/lib/graphiti/schema/check.rb +69 -0
- data/lib/graphiti/schema.rb +8 -9
- data/lib/graphiti/scope.rb +4 -5
- data/lib/graphiti/sideload.rb +46 -9
- data/lib/graphiti/spec_helpers/matchers.rb +2 -1
- data/lib/graphiti/spec_helpers/rspec.rb +5 -13
- data/lib/graphiti/util/serializer_relationships.rb +1 -10
- data/lib/graphiti/util/simple_errors.rb +26 -5
- data/lib/graphiti/util/transaction_hooks_recorder.rb +2 -2
- data/lib/graphiti/version.rb +1 -1
- data/lib/graphiti.rb +9 -4
- data/lib/tasks/graphiti.rake +18 -0
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2963ad9851cf238b3885921c2b4858bf3b7aefe1fad5726a588c966da8cd0ada
|
|
4
|
+
data.tar.gz: 6486ac49fff4aa22faaa46cc2aef5439a015943748fd323288c8f90f596fbf9a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b25b468d3ddf0efc6aa513940ae06c2cc30150e7d4a743bd5ba1d4a56a768f3a7452780b515bbd68610ab51bdd979d577f7644d83a0fa724e975a45a547e1bef
|
|
7
|
+
data.tar.gz: 02a6b4f82e8e7c2d9582c8765d49ef07901e001ea942846d3b70f79ea25c7e83d4d032c5644404420c075e0565eb946cfb1d54356ed319aefb2841e644bb0944
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -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
|
-
|
|
54
|
-
|
|
53
|
+
hint = setting[:note] || setting[:values]&.map(&:inspect)
|
|
54
|
+
&.to_sentence(two_words_connector: " or ", last_word_connector: ", or ")
|
|
55
55
|
|
|
56
|
-
setting[:
|
|
57
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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 %>.
|
|
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.
|
|
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[:
|
|
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
|
data/lib/graphiti/debugger.rb
CHANGED
|
@@ -11,13 +11,13 @@ module Graphiti
|
|
|
11
11
|
private_constant :CHUNKS
|
|
12
12
|
|
|
13
13
|
class << self
|
|
14
|
-
# Pool threads inherit this through the
|
|
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
|
-
|
|
16
|
+
Fiber[CHUNKS] ||= Concurrent::Array.new
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def chunks=(value)
|
|
20
|
-
|
|
20
|
+
Fiber[CHUNKS] = value
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def on_data(name, start, stop, id, payload)
|
|
@@ -1,25 +1,15 @@
|
|
|
1
|
-
#
|
|
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
|
-
#
|
|
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
|
|
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
|
-
|
|
22
|
-
`
|
|
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
|
|
53
|
+
def default_title
|
|
52
54
|
"Request Error"
|
|
53
55
|
end
|
|
54
56
|
|
|
@@ -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,
|
|
18
|
+
each_error do |attribute, message, validation_code|
|
|
17
19
|
errors << {
|
|
18
|
-
code:
|
|
20
|
+
code: code,
|
|
19
21
|
status: "422",
|
|
20
|
-
title:
|
|
22
|
+
title: title,
|
|
21
23
|
detail: detail_for(attribute, message),
|
|
22
24
|
source: {pointer: pointer_for(object, attribute)},
|
|
23
|
-
meta: meta_for(attribute, message,
|
|
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]
|
data/lib/graphiti/query.rb
CHANGED
|
@@ -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[:
|
|
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
|
data/lib/graphiti/rails.rb
CHANGED
|
@@ -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: []
|