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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +6 -0
  3. data/.github/workflows/release.yml +2 -0
  4. data/.tool-versions +1 -1
  5. data/CHANGELOG.md +39 -0
  6. data/Rakefile +17 -12
  7. data/lib/generators/graphiti/generator_mixin.rb +5 -4
  8. data/lib/generators/graphiti/install_generator.rb +3 -0
  9. data/lib/generators/graphiti/locale_generator.rb +30 -0
  10. data/lib/generators/graphiti/templates/locale.yml.erb +22 -0
  11. data/lib/graphiti/adapters/abstract.rb +4 -3
  12. data/lib/graphiti/error_serializers/conflict_request.rb +1 -1
  13. data/lib/graphiti/error_serializers/deprecated_constants.rb +6 -16
  14. data/lib/graphiti/error_serializers/invalid_request.rb +6 -0
  15. data/lib/graphiti/errors.rb +4 -4
  16. data/lib/graphiti/query.rb +27 -0
  17. data/lib/graphiti/rails/exception_handlers.rb +17 -0
  18. data/lib/graphiti/rails/railtie.rb +10 -0
  19. data/lib/graphiti/rails/rake_helpers.rb +5 -0
  20. data/lib/graphiti/rails.rb +18 -0
  21. data/lib/graphiti/renderer.rb +4 -1
  22. data/lib/graphiti/request_validators/update_validator.rb +1 -5
  23. data/lib/graphiti/request_validators/validator.rb +6 -6
  24. data/lib/graphiti/resource/configuration.rb +11 -10
  25. data/lib/graphiti/resource/dsl.rb +5 -5
  26. data/lib/graphiti/resource/links.rb +3 -0
  27. data/lib/graphiti/schema/check.rb +69 -0
  28. data/lib/graphiti/schema.rb +8 -9
  29. data/lib/graphiti/scope.rb +51 -22
  30. data/lib/graphiti/scoping/filter.rb +2 -2
  31. data/lib/graphiti/serializer.rb +25 -9
  32. data/lib/graphiti/sideload/polymorphic_belongs_to.rb +9 -9
  33. data/lib/graphiti/sideload.rb +36 -25
  34. data/lib/graphiti/spec_helpers/matchers.rb +1 -1
  35. data/lib/graphiti/spec_helpers/rspec.rb +5 -13
  36. data/lib/graphiti/util/simple_errors.rb +26 -5
  37. data/lib/graphiti/version.rb +1 -1
  38. data/lib/graphiti.rb +1 -0
  39. data/lib/tasks/graphiti.rake +18 -0
  40. data/package.json +1 -1
  41. metadata +4 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b1e331ab2dea47987ea47260eb0890c9a953d3435bba2e7ef37cbfbbc83fe5ab
4
- data.tar.gz: 13a7483ead9f4ad5ed69390ff6a4a122a819813c2147c0a56dceebdfebc57e17
3
+ metadata.gz: 6e73d0272cfabe57d3eb0cbc82ad7199dea624afead6856ca72c1107857fa144
4
+ data.tar.gz: 795ddb1e77c8f61d2e74c66c333ccc4acd5c37ccb6aa3b4c7cb816f52dfe1b71
5
5
  SHA512:
6
- metadata.gz: cca252389b52b5a2983407b4eff7cf7fe8b49759b478228f9d78303522bc40f2948a1784d6c863373f10521dc80b61d35f201009e830d8a968cc989b37d2ad81
7
- data.tar.gz: 3fdfda722d09ddea6c366ca4102258bdea6d36c923be729ae141063e7e62b43ae0730f30a6422ae4448f858a5c1d4269f80180279d7939ce050c88008613cedb
6
+ metadata.gz: f5ff1c0630702209372d0512afd16d29c87d6f561c551e08a5080bd25efa8b3bae465d33729129289a3ade41db781b779bffd753b55f6cf7cf02389662fa835e
7
+ data.tar.gz: '0785ff7bd32570ef7bf588c7287b4704d7f9daf50bafb47f0739f02b054086a297cef4427364f818b5ab540a72935447bead5898957f597a400a40b435f4a81f'
@@ -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
@@ -13,6 +13,8 @@ jobs:
13
13
  uses: actions/checkout@v5
14
14
  with:
15
15
  persist-credentials: false
16
+ # semantic-release reads the tag history to work out the next version
17
+ fetch-depth: 0
16
18
  - name: Set up Ruby
17
19
  uses: ruby/setup-ruby@v1
18
20
  with:
data/.tool-versions CHANGED
@@ -1 +1 @@
1
- ruby 3.3.4
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 "Compare the working tree to the last release, and plot it"
21
- task :current do
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 "Plot the release history plus the working tree to tmp/performance.html and open it"
26
- task :page do
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 "Measure one release and record it (TAG=v2.0.0-beta.9)"
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
- tag = ENV["TAG"] or abort "pass the release to record, e.g. rake performance:record TAG=v2.0.0-beta.9"
33
- measure(tag)
34
+ measure(*(if ENV["TAG"]
35
+ [ENV["TAG"]]
36
+ else
37
+ ENV["ALL"] ? ["--all"] : ["--missing"]
38
+ end))
34
39
  end
35
40
 
36
- desc "Re-record every release for this ruby, replacing its rows"
37
- task :record_all do
38
- measure("--all")
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
- 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
@@ -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
- if parent.send(:"#{association_name}").nil?
374
+ associated = parent.send(:"#{association_name}")
375
+ if associated.nil?
375
376
  parent.send(:"#{association_name}=", [child])
376
- else
377
- parent.send(:"#{association_name}") << child
377
+ elsif associated.none? { |existing| existing.equal?(child) }
378
+ associated << child
378
379
  end
379
380
  else
380
381
  parent.send(:"#{association_name}=", child)
@@ -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
 
@@ -49,6 +49,12 @@ module Graphiti
49
49
  end
50
50
 
51
51
  def title
52
+ return default_title unless defined?(::I18n)
53
+
54
+ ::I18n.t :title, scope: [:graphiti, :errors, code], default: default_title
55
+ end
56
+
57
+ def default_title
52
58
  "Request Error"
53
59
  end
54
60
 
@@ -719,11 +719,11 @@ module Graphiti
719
719
 
720
720
  def message
721
721
  <<~MSG
722
- #{@resource_class.name}: #{@attribute} must be one of :as_literal, :as_nil, or :reject. Got #{@value.inspect}.
722
+ #{@resource_class.name}: #{@attribute} must be one of :literal, :null, or :rejected. Got #{@value.inspect}.
723
723
 
724
- :as_literal - "null" and "" reach the filter as strings (default)
725
- :as_nil - "null" becomes nil, so the filter can query for NULL
726
- :reject - a blank value raises InvalidFilterValue
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
@@ -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
@@ -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: []
@@ -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).to_json
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, message: "must be an object")
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, message: "must be an object")
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, message: "is not a valid 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, message: "is an 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, message: "cannot be written")
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, message: "should be type #{e.type_name}")
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 = [:as_literal, :as_nil, :reject].freeze
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
- filters_blanks_by_default: {
32
- default: :as_literal,
32
+ filter_blanks_treated_as: {
33
+ default: :literal,
33
34
  values: BLANK_MODES,
34
- invalid: ->(klass, value) { Errors::InvalidFilterBlanks.new(klass, :filters_blanks_by_default, value) }
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 filters_blanks_by_default. Remove in 3.0.
198
+ # Deprecated. Both folded into filter_blanks_treated_as. Remove in 3.0.
198
199
  def filters_accept_nil_by_default
199
- filters_blanks_by_default == :as_nil
200
+ filter_blanks_treated_as == :null
200
201
  end
201
202
 
202
203
  def filters_accept_nil_by_default=(val)
203
- self.filters_blanks_by_default = val ? :as_nil : :as_literal
204
+ self.filter_blanks_treated_as = val ? :null : :literal
204
205
  end
205
206
 
206
207
  def filters_deny_empty_by_default
207
- filters_blanks_by_default == :reject
208
+ filter_blanks_treated_as == :rejected
208
209
  end
209
210
 
210
211
  def filters_deny_empty_by_default=(val)
211
- self.filters_blanks_by_default = val ? :reject : :as_literal
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.filters_blanks_by_default` (:as_literal, :as_nil, or :reject)"
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: :reject.")
212
- return :reject if opts[:deny_empty]
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: :as_nil.")
217
- return opts[:allow_nil] ? :as_nil : :as_literal
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, filters_blanks_by_default)
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