graphiti 2.0.0.beta.10 → 2.0.0.beta.12
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/.github/workflows/release.yml +2 -0
- data/.tool-versions +1 -1
- data/CHANGELOG.md +39 -0
- data/Rakefile +17 -12
- data/lib/generators/graphiti/generator_mixin.rb +5 -4
- data/lib/generators/graphiti/install_generator.rb +3 -0
- data/lib/generators/graphiti/locale_generator.rb +30 -0
- data/lib/generators/graphiti/templates/locale.yml.erb +22 -0
- data/lib/graphiti/adapters/abstract.rb +4 -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 +6 -0
- data/lib/graphiti/errors.rb +4 -4
- data/lib/graphiti/query.rb +27 -0
- data/lib/graphiti/rails/exception_handlers.rb +17 -0
- data/lib/graphiti/rails/railtie.rb +10 -0
- data/lib/graphiti/rails/rake_helpers.rb +5 -0
- data/lib/graphiti/rails.rb +18 -0
- data/lib/graphiti/renderer.rb +4 -1
- data/lib/graphiti/request_validators/update_validator.rb +1 -5
- data/lib/graphiti/request_validators/validator.rb +6 -6
- data/lib/graphiti/resource/configuration.rb +11 -10
- data/lib/graphiti/resource/dsl.rb +5 -5
- data/lib/graphiti/resource/links.rb +3 -0
- data/lib/graphiti/schema/check.rb +69 -0
- data/lib/graphiti/schema.rb +8 -9
- data/lib/graphiti/scope.rb +51 -22
- data/lib/graphiti/scoping/filter.rb +2 -2
- data/lib/graphiti/serializer.rb +25 -9
- data/lib/graphiti/sideload/polymorphic_belongs_to.rb +9 -9
- data/lib/graphiti/sideload.rb +36 -25
- data/lib/graphiti/spec_helpers/matchers.rb +1 -1
- data/lib/graphiti/spec_helpers/rspec.rb +5 -13
- data/lib/graphiti/util/simple_errors.rb +26 -5
- data/lib/graphiti/version.rb +1 -1
- data/lib/graphiti.rb +1 -0
- data/lib/tasks/graphiti.rake +18 -0
- data/package.json +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6e73d0272cfabe57d3eb0cbc82ad7199dea624afead6856ca72c1107857fa144
|
|
4
|
+
data.tar.gz: 795ddb1e77c8f61d2e74c66c333ccc4acd5c37ccb6aa3b4c7cb816f52dfe1b71
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f5ff1c0630702209372d0512afd16d29c87d6f561c551e08a5080bd25efa8b3bae465d33729129289a3ade41db781b779bffd753b55f6cf7cf02389662fa835e
|
|
7
|
+
data.tar.gz: '0785ff7bd32570ef7bf588c7287b4704d7f9daf50bafb47f0739f02b054086a297cef4427364f818b5ab540a72935447bead5898957f597a400a40b435f4a81f'
|
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/.tool-versions
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
ruby
|
|
1
|
+
ruby 4.0.6
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
graphiti changelog
|
|
2
2
|
|
|
3
|
+
# [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)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* compare each concurrency path against itself in the table to better illustrate the performance wins ([d0f0e92](https://github.com/graphiti-api/graphiti/commit/d0f0e92860b3b4affe3fca6dbb9b871fe8a1374f))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* add schema rake tasks, so checking a schema no longer means running the suite ([6f4783d](https://github.com/graphiti-api/graphiti/commit/6f4783d5654659125b7eb358bf1175b6feaacb7c))
|
|
14
|
+
* declare Graphiti's client errors in rescue_responses ([e369824](https://github.com/graphiti-api/graphiti/commit/e369824d6e11b671ffd212caad3d0a43a7ceefcf))
|
|
15
|
+
* mark deprecated settings in the generated ApplicationResource ([52b0642](https://github.com/graphiti-api/graphiti/commit/52b0642e689a16d14dacaf4e28a2641f10774cf0))
|
|
16
|
+
* 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)
|
|
17
|
+
|
|
18
|
+
# [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)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Bug Fixes
|
|
22
|
+
|
|
23
|
+
* always render included when the client asked to include ([8691b3f](https://github.com/graphiti-api/graphiti/commit/8691b3fd08f3cae0481ba72839911fdc71ef5634))
|
|
24
|
+
* autolink = true no longer clobbers an inherited :on_demand ([2d26f8c](https://github.com/graphiti-api/graphiti/commit/2d26f8cb567e82a16918345de9114b8ceef64523))
|
|
25
|
+
* read the links param from the query, not the context object ([3b934cb](https://github.com/graphiti-api/graphiti/commit/3b934cb25be049e1e80ba400123da944cfd23d04))
|
|
26
|
+
* stop assigning the same child twice when a resource repeats in the include path ([588503b](https://github.com/graphiti-api/graphiti/commit/588503b109508495d8170373d80019c6971f7d9c))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
### Features
|
|
30
|
+
|
|
31
|
+
* omit relationships that render no ids and no link ([916aa8c](https://github.com/graphiti-api/graphiti/commit/916aa8c9c596c5d9b6c802c5ab13b766a13d1e70))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
### Performance Improvements
|
|
35
|
+
|
|
36
|
+
* memoize whether a belongs_to renders ids from its foreign key ([dcb60b2](https://github.com/graphiti-api/graphiti/commit/dcb60b2096c414756ccff534773d2e45594f0144))
|
|
37
|
+
* only deduplicate resources that two include paths can both reach ([744f014](https://github.com/graphiti-api/graphiti/commit/744f014249382bc25d5ea43f692778ad7adc25c3))
|
|
38
|
+
* resolve a lone sideload inline rather than through the pool ([3415592](https://github.com/graphiti-api/graphiti/commit/3415592d75900389a8f11f53b65ab399e297e4a9))
|
|
39
|
+
* reuse the resolved data and proxies when building cache keys ([8a17350](https://github.com/graphiti-api/graphiti/commit/8a17350542ba0062fdcddd115afe8041b62198c7))
|
|
40
|
+
* skip the on-demand link strip when no relationship renders links on demand ([cf86a8c](https://github.com/graphiti-api/graphiti/commit/cf86a8c249077d9f9d5a13de49989d102bbaed04))
|
|
41
|
+
|
|
3
42
|
# [2.0.0-beta.10](https://github.com/graphiti-api/graphiti/compare/v2.0.0-beta.9...v2.0.0-beta.10) (2026-08-24)
|
|
4
43
|
|
|
5
44
|
|
data/Rakefile
CHANGED
|
@@ -11,31 +11,36 @@ RSpec::Core::RakeTask.new(:spec) do |t|
|
|
|
11
11
|
end
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
# Allocations mean the same anywhere and timings do not, so the history has a
|
|
15
|
+
# reference CPU, named on every row, and recording timings refuses to mix them.
|
|
14
16
|
namespace :performance do
|
|
15
17
|
# Shelling out keeps a failure to the script's own message, with no rake backtrace on top.
|
|
16
18
|
def measure(*arguments)
|
|
17
19
|
exit(1) unless system("bundle", "exec", "ruby", "spec/performance/measure_releases.rb", *arguments)
|
|
18
20
|
end
|
|
19
21
|
|
|
20
|
-
desc "
|
|
21
|
-
task :
|
|
22
|
-
measure
|
|
22
|
+
desc "Read a change: measure the working tree, print the comparison and plot it"
|
|
23
|
+
task :read do
|
|
24
|
+
measure("--read")
|
|
23
25
|
end
|
|
24
26
|
|
|
25
|
-
desc "
|
|
26
|
-
task :
|
|
27
|
-
exit(1) unless system("bundle", "exec", "ruby", "spec/performance/chart_page.rb")
|
|
27
|
+
desc "Open the recorded history as a chart. Any machine, measures nothing"
|
|
28
|
+
task :chart do
|
|
29
|
+
exit(1) unless system("bundle", "exec", "ruby", "spec/performance/chart_page.rb", "--no-current")
|
|
28
30
|
end
|
|
29
31
|
|
|
30
|
-
desc "
|
|
32
|
+
desc "Fill in the history. Missing releases by default, TAG=v2.0.0-beta.9 for one, ALL=1 for every. Reference CPU"
|
|
31
33
|
task :record do
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
measure(*(if ENV["TAG"]
|
|
35
|
+
[ENV["TAG"]]
|
|
36
|
+
else
|
|
37
|
+
ENV["ALL"] ? ["--all"] : ["--missing"]
|
|
38
|
+
end))
|
|
34
39
|
end
|
|
35
40
|
|
|
36
|
-
desc "
|
|
37
|
-
task :
|
|
38
|
-
measure("--
|
|
41
|
+
desc "Measure the working tree for the release to name, and commit it. Reference CPU"
|
|
42
|
+
task :pending do
|
|
43
|
+
measure("--pending")
|
|
39
44
|
end
|
|
40
45
|
end
|
|
41
46
|
|
|
@@ -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
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
desc "Writes the error text Graphiti renders, ready to edit"
|
|
12
|
+
def locale
|
|
13
|
+
template("locale.yml.erb", File.join("config/locales", "graphiti.en.yml"))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def omit_comments?
|
|
19
|
+
@options["omit-comments"]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def default_messages
|
|
23
|
+
Graphiti::Util::SimpleErrors::DEFAULT_MESSAGES
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def default_format
|
|
27
|
+
Graphiti::Util::SimpleErrors::DEFAULT_FORMAT
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<%- unless omit_comments? -%>
|
|
2
|
+
# Everything Graphiti puts in an errors payload.
|
|
3
|
+
# For another language, add a block below or a graphiti.<locale>.yml file.
|
|
4
|
+
<%- end -%>
|
|
5
|
+
en:
|
|
6
|
+
graphiti:
|
|
7
|
+
errors:
|
|
8
|
+
<%- unless omit_comments? -%>
|
|
9
|
+
# Titles and details are keyed by error code, and Graphiti ships none.
|
|
10
|
+
# Without a key you get the HTTP status name and no detail.
|
|
11
|
+
#
|
|
12
|
+
# internal_server_error:
|
|
13
|
+
# title: "Something went wrong"
|
|
14
|
+
# detail: "We've probably received an error report already, but please contact us if the issue persists."
|
|
15
|
+
#
|
|
16
|
+
# format joins them: "data.attributes.title is an unknown attribute".
|
|
17
|
+
<%- end -%>
|
|
18
|
+
format: <%= default_format.inspect %>
|
|
19
|
+
messages:
|
|
20
|
+
<%- default_messages.each do |code, message| -%>
|
|
21
|
+
<%= code %>: <%= message.inspect %>
|
|
22
|
+
<%- end -%>
|
|
@@ -371,10 +371,11 @@ module Graphiti
|
|
|
371
371
|
activerecord_adapter.associate \
|
|
372
372
|
parent, child, association_name, association_type
|
|
373
373
|
elsif [:has_many, :many_to_many].include?(association_type)
|
|
374
|
-
|
|
374
|
+
associated = parent.send(:"#{association_name}")
|
|
375
|
+
if associated.nil?
|
|
375
376
|
parent.send(:"#{association_name}=", [child])
|
|
376
|
-
|
|
377
|
-
|
|
377
|
+
elsif associated.none? { |existing| existing.equal?(child) }
|
|
378
|
+
associated << child
|
|
378
379
|
end
|
|
379
380
|
else
|
|
380
381
|
parent.send(:"#{association_name}=", child)
|
|
@@ -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
|
|
data/lib/graphiti/errors.rb
CHANGED
|
@@ -719,11 +719,11 @@ module Graphiti
|
|
|
719
719
|
|
|
720
720
|
def message
|
|
721
721
|
<<~MSG
|
|
722
|
-
#{@resource_class.name}: #{@attribute} must be one of :
|
|
722
|
+
#{@resource_class.name}: #{@attribute} must be one of :literal, :null, or :rejected. Got #{@value.inspect}.
|
|
723
723
|
|
|
724
|
-
:
|
|
725
|
-
:
|
|
726
|
-
:
|
|
724
|
+
:literal - "null" and "" reach the filter as strings (default)
|
|
725
|
+
:null - "null" becomes nil, so the filter can query for NULL
|
|
726
|
+
:rejected - a blank value raises InvalidFilterValue
|
|
727
727
|
MSG
|
|
728
728
|
end
|
|
729
729
|
end
|
data/lib/graphiti/query.rb
CHANGED
|
@@ -39,10 +39,37 @@ module Graphiti
|
|
|
39
39
|
@entity_map
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
# Deduplication exists for a row two include paths both resolve, so a
|
|
43
|
+
# resource class sitting at one node has nothing to collide with.
|
|
44
|
+
def repeated_resource_classes
|
|
45
|
+
return root.repeated_resource_classes unless root == self
|
|
46
|
+
|
|
47
|
+
@repeated_resource_classes ||= begin
|
|
48
|
+
counts = Hash.new(0)
|
|
49
|
+
walk = [self]
|
|
50
|
+
while (query = walk.pop)
|
|
51
|
+
counts[query.resource.class] += 1
|
|
52
|
+
query.sideloads.each_pair do |name, sideload_query|
|
|
53
|
+
walk << sideload_query
|
|
54
|
+
sideload = query.resource.class.sideload(name)
|
|
55
|
+
next unless sideload.respond_to?(:children)
|
|
56
|
+
|
|
57
|
+
# A polymorphic child resolves under a class no node names.
|
|
58
|
+
sideload.children.each_value { |child| counts[child.resource.class] += 2 }
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
counts.select { |_, count| count > 1 }.keys.to_set
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
42
65
|
def top_level?
|
|
43
66
|
!association?
|
|
44
67
|
end
|
|
45
68
|
|
|
69
|
+
def includes_requested?
|
|
70
|
+
@include_param.present?
|
|
71
|
+
end
|
|
72
|
+
|
|
46
73
|
def links_requested?
|
|
47
74
|
return @links_requested if defined?(@links_requested)
|
|
48
75
|
|
|
@@ -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
|
|
@@ -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: []
|
data/lib/graphiti/renderer.rb
CHANGED
|
@@ -13,8 +13,11 @@ module Graphiti
|
|
|
13
13
|
@records ||= @proxy.data
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
# JSON:API requires the key even when the include turns up nothing.
|
|
16
17
|
def to_jsonapi
|
|
17
|
-
render(self.class.jsonapi_renderer)
|
|
18
|
+
rendered = render(self.class.jsonapi_renderer)
|
|
19
|
+
rendered[:included] ||= [] if proxy.query.includes_requested?
|
|
20
|
+
rendered.to_json
|
|
18
21
|
end
|
|
19
22
|
|
|
20
23
|
def as_graphql
|
|
@@ -13,11 +13,7 @@ module Graphiti
|
|
|
13
13
|
|
|
14
14
|
def attribute_mismatch(attr_path)
|
|
15
15
|
@error_class = Graphiti::Errors::ConflictRequest
|
|
16
|
-
@errors.add(
|
|
17
|
-
attr_path.join("."),
|
|
18
|
-
:attribute_mismatch,
|
|
19
|
-
message: "does not match the server endpoint"
|
|
20
|
-
)
|
|
16
|
+
@errors.add(attr_path.join("."), :attribute_mismatch)
|
|
21
17
|
end
|
|
22
18
|
|
|
23
19
|
def required_payload?
|
|
@@ -54,14 +54,14 @@ module Graphiti
|
|
|
54
54
|
page = @params[:page]
|
|
55
55
|
return if page.nil? || page.respond_to?(:each_pair)
|
|
56
56
|
|
|
57
|
-
@errors.add(:page, :invalid
|
|
57
|
+
@errors.add(:page, :invalid)
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
def validate_data_shape
|
|
61
61
|
data = @params[:data]
|
|
62
62
|
return true if data.nil? || data.respond_to?(:each_pair)
|
|
63
63
|
|
|
64
|
-
@errors.add(:data, :invalid
|
|
64
|
+
@errors.add(:data, :invalid)
|
|
65
65
|
false
|
|
66
66
|
end
|
|
67
67
|
|
|
@@ -69,7 +69,7 @@ module Graphiti
|
|
|
69
69
|
relationships.each_key do |name|
|
|
70
70
|
unless resource.class.sideload(name.to_sym)
|
|
71
71
|
full_key = fully_qualified_key(name, payload_path, :relationships)
|
|
72
|
-
@errors.add(full_key, :invalid_relationship
|
|
72
|
+
@errors.add(full_key, :invalid_relationship)
|
|
73
73
|
end
|
|
74
74
|
end
|
|
75
75
|
|
|
@@ -110,11 +110,11 @@ module Graphiti
|
|
|
110
110
|
begin
|
|
111
111
|
attributes[key] = resource.typecast(key, value, :writable)
|
|
112
112
|
rescue Graphiti::Errors::UnknownAttribute
|
|
113
|
-
@errors.add(fully_qualified_key(key, payload_path), :unknown_attribute
|
|
113
|
+
@errors.add(fully_qualified_key(key, payload_path), :unknown_attribute)
|
|
114
114
|
rescue Graphiti::Errors::InvalidAttributeAccess
|
|
115
|
-
@errors.add(fully_qualified_key(key, payload_path), :unwritable_attribute
|
|
115
|
+
@errors.add(fully_qualified_key(key, payload_path), :unwritable_attribute)
|
|
116
116
|
rescue Graphiti::Errors::TypecastFailed => e
|
|
117
|
-
@errors.add(fully_qualified_key(key, payload_path), :type_error,
|
|
117
|
+
@errors.add(fully_qualified_key(key, payload_path), :type_error, type: e.type_name)
|
|
118
118
|
end
|
|
119
119
|
end
|
|
120
120
|
end
|
|
@@ -6,7 +6,7 @@ module Graphiti
|
|
|
6
6
|
DEFAULT_MAX_PAGE_SIZE = 1_000
|
|
7
7
|
LINK_MODES = [true, false, :on_demand].freeze
|
|
8
8
|
BELONGS_TO_RESOURCE_IDS_MODES = [:foreign_key, :always, :never].freeze
|
|
9
|
-
BLANK_MODES = [:
|
|
9
|
+
BLANK_MODES = [:literal, :null, :rejected].freeze
|
|
10
10
|
|
|
11
11
|
# Grouped for the ApplicationResource the install generator writes.
|
|
12
12
|
SETTING_GROUPS = { # :nodoc:
|
|
@@ -21,6 +21,7 @@ module Graphiti
|
|
|
21
21
|
relationships: {
|
|
22
22
|
relationships_readable_by_default: {default: true},
|
|
23
23
|
relationships_writable_by_default: {default: true},
|
|
24
|
+
relationship_placeholders: {default: false, note: "render placeholders for relationships with no ids and no link"},
|
|
24
25
|
belongs_to_resource_ids_by_default: {
|
|
25
26
|
default: :foreign_key,
|
|
26
27
|
values: BELONGS_TO_RESOURCE_IDS_MODES,
|
|
@@ -28,10 +29,10 @@ module Graphiti
|
|
|
28
29
|
}
|
|
29
30
|
},
|
|
30
31
|
filters: {
|
|
31
|
-
|
|
32
|
-
default: :
|
|
32
|
+
filter_blanks_treated_as: {
|
|
33
|
+
default: :literal,
|
|
33
34
|
values: BLANK_MODES,
|
|
34
|
-
invalid: ->(klass, value) { Errors::InvalidFilterBlanks.new(klass, :
|
|
35
|
+
invalid: ->(klass, value) { Errors::InvalidFilterBlanks.new(klass, :filter_blanks_treated_as, value) }
|
|
35
36
|
}
|
|
36
37
|
},
|
|
37
38
|
sorting: {
|
|
@@ -194,21 +195,21 @@ module Graphiti
|
|
|
194
195
|
end
|
|
195
196
|
|
|
196
197
|
class_methods do
|
|
197
|
-
# Deprecated. Both folded into
|
|
198
|
+
# Deprecated. Both folded into filter_blanks_treated_as. Remove in 3.0.
|
|
198
199
|
def filters_accept_nil_by_default
|
|
199
|
-
|
|
200
|
+
filter_blanks_treated_as == :null
|
|
200
201
|
end
|
|
201
202
|
|
|
202
203
|
def filters_accept_nil_by_default=(val)
|
|
203
|
-
self.
|
|
204
|
+
self.filter_blanks_treated_as = val ? :null : :literal
|
|
204
205
|
end
|
|
205
206
|
|
|
206
207
|
def filters_deny_empty_by_default
|
|
207
|
-
|
|
208
|
+
filter_blanks_treated_as == :rejected
|
|
208
209
|
end
|
|
209
210
|
|
|
210
211
|
def filters_deny_empty_by_default=(val)
|
|
211
|
-
self.
|
|
212
|
+
self.filter_blanks_treated_as = val ? :rejected : :literal
|
|
212
213
|
end
|
|
213
214
|
|
|
214
215
|
# Deprecated. Renamed to the page_ family. Remove in 3.0.
|
|
@@ -416,7 +417,7 @@ module Graphiti
|
|
|
416
417
|
end
|
|
417
418
|
end
|
|
418
419
|
|
|
419
|
-
blanks_msg = "Use `self.
|
|
420
|
+
blanks_msg = "Use `self.filter_blanks_treated_as` (:literal, :null, or :rejected)"
|
|
420
421
|
page_msg = "Use `self.page_default_size`, `self.page_max_size` and `self.page_cursors`"
|
|
421
422
|
DEPRECATOR.deprecate_methods(Configuration::ClassMethods,
|
|
422
423
|
filters_accept_nil_by_default: blanks_msg,
|
|
@@ -208,16 +208,16 @@ module Graphiti
|
|
|
208
208
|
|
|
209
209
|
def blanks_for(name, opts)
|
|
210
210
|
if opts.key?(:deny_empty)
|
|
211
|
-
DEPRECATOR.warn("The deny_empty: filter option is deprecated. Use blanks: :
|
|
212
|
-
return :
|
|
211
|
+
DEPRECATOR.warn("The deny_empty: filter option is deprecated. Use blanks: :rejected.")
|
|
212
|
+
return :rejected if opts[:deny_empty]
|
|
213
213
|
end
|
|
214
214
|
|
|
215
215
|
if opts.key?(:allow_nil)
|
|
216
|
-
DEPRECATOR.warn("The allow_nil: filter option is deprecated. Use blanks: :
|
|
217
|
-
return opts[:allow_nil] ? :
|
|
216
|
+
DEPRECATOR.warn("The allow_nil: filter option is deprecated. Use blanks: :null.")
|
|
217
|
+
return opts[:allow_nil] ? :null : :literal
|
|
218
218
|
end
|
|
219
219
|
|
|
220
|
-
blanks = opts.fetch(:blanks,
|
|
220
|
+
blanks = opts.fetch(:blanks, filter_blanks_treated_as)
|
|
221
221
|
unless Resource::BLANK_MODES.include?(blanks)
|
|
222
222
|
raise Errors::InvalidFilterBlanks.new(self, name, blanks)
|
|
223
223
|
end
|
|
@@ -39,7 +39,10 @@ module Graphiti
|
|
|
39
39
|
relationship_links != false
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
# true cannot express :on_demand, so it leaves an inherited on-demand mode alone.
|
|
42
43
|
def autolink=(val)
|
|
44
|
+
return if val && relationship_links == :on_demand
|
|
45
|
+
|
|
43
46
|
self.relationship_links = val ? true : false
|
|
44
47
|
end
|
|
45
48
|
|