graphiti 2.0.0.beta.11 → 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/CHANGELOG.md +15 -0
- 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/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/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/request_validators/update_validator.rb +1 -5
- data/lib/graphiti/request_validators/validator.rb +6 -6
- data/lib/graphiti/schema/check.rb +69 -0
- data/lib/graphiti/schema.rb +8 -9
- 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
- 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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
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
|
+
|
|
3
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)
|
|
4
19
|
|
|
5
20
|
|
|
@@ -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 -%>
|
|
@@ -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
|
|
|
@@ -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: []
|
|
@@ -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
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
module Graphiti
|
|
2
|
+
class Schema
|
|
3
|
+
class Check
|
|
4
|
+
attr_reader :path, :errors
|
|
5
|
+
|
|
6
|
+
def initialize(schema, path)
|
|
7
|
+
@path = path
|
|
8
|
+
@schema = schema
|
|
9
|
+
@generated = normalize(schema)
|
|
10
|
+
@committed = normalize(JSON.parse(File.read(path))) if File.exist?(path)
|
|
11
|
+
@errors = @committed ? SchemaDiff.new(@committed, @generated).compare : []
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def missing?
|
|
15
|
+
@committed.nil?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def stale?
|
|
19
|
+
!missing? && @committed != @generated
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def compatible?
|
|
23
|
+
errors.empty?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def ok?
|
|
27
|
+
!missing? && !stale? && compatible?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def write!
|
|
31
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
32
|
+
File.write(path, JSON.pretty_generate(@schema))
|
|
33
|
+
path
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def message
|
|
37
|
+
return "Schema is up to date: #{path}" if ok?
|
|
38
|
+
return "#{missing_message}\n\n#{regenerate}" if missing?
|
|
39
|
+
return "#{incompatible_message}\n\n#{errors.join("\n")}" unless compatible?
|
|
40
|
+
|
|
41
|
+
"Schema file is outdated: #{path}\n\n#{regenerate}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def normalize(schema)
|
|
47
|
+
JSON.parse(JSON.generate(schema))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def missing_message
|
|
51
|
+
"Schema file not found: #{path}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def incompatible_message
|
|
55
|
+
<<~MSG.chomp
|
|
56
|
+
Found backwards-incompatibilities in schema: #{path}
|
|
57
|
+
|
|
58
|
+
Re-run with FORCE_SCHEMA=true to accept them and overwrite the file.
|
|
59
|
+
|
|
60
|
+
Incompatibilities:
|
|
61
|
+
MSG
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def regenerate
|
|
65
|
+
"Run `rake graphiti:schema:generate` and commit the file."
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
data/lib/graphiti/schema.rb
CHANGED
|
@@ -11,19 +11,18 @@ module Graphiti
|
|
|
11
11
|
new(resources).generate
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
def self.generate!(resources = nil)
|
|
15
|
-
|
|
14
|
+
def self.generate!(resources = nil, path: Graphiti.config.schema_path, force: ENV["FORCE_SCHEMA"] == "true")
|
|
15
|
+
result = check(resources, path: path)
|
|
16
|
+
return result.errors if !force && !result.compatible?
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
old = JSON.parse(File.read(Graphiti.config.schema_path))
|
|
19
|
-
errors = Graphiti::SchemaDiff.new(old, schema).compare
|
|
20
|
-
return errors if errors.any?
|
|
21
|
-
end
|
|
22
|
-
FileUtils.mkdir_p(Graphiti.config.schema_path.to_s.gsub("/schema.json", ""))
|
|
23
|
-
File.write(Graphiti.config.schema_path, JSON.pretty_generate(schema))
|
|
18
|
+
result.write!
|
|
24
19
|
[]
|
|
25
20
|
end
|
|
26
21
|
|
|
22
|
+
def self.check(resources = nil, path: Graphiti.config.schema_path)
|
|
23
|
+
Check.new(generate(resources), path)
|
|
24
|
+
end
|
|
25
|
+
|
|
27
26
|
def initialize(resources)
|
|
28
27
|
@resources = resources.sort_by(&:name)
|
|
29
28
|
@remote_resources = @resources.select(&:remote?)
|
|
@@ -127,22 +127,14 @@ module Graphiti
|
|
|
127
127
|
end
|
|
128
128
|
end
|
|
129
129
|
|
|
130
|
-
def self.schema!(resources = nil)
|
|
130
|
+
def self.schema!(resources = nil, path: nil)
|
|
131
131
|
::RSpec.describe "Graphiti Schema" do
|
|
132
132
|
it "generates a backwards-compatible schema" do
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
check = Graphiti::Schema.check(resources, path: path || Graphiti.config.schema_path)
|
|
134
|
+
forced = ENV["FORCE_SCHEMA"] == "true"
|
|
135
|
+
check.write! if check.compatible? || forced
|
|
135
136
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
MSG
|
|
139
|
-
|
|
140
|
-
errors = Graphiti::Schema.generate!(resources)
|
|
141
|
-
errors.each do |e|
|
|
142
|
-
message << "#{e}\n"
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
expect(errors.empty?).to eq(true), message
|
|
137
|
+
expect(check.compatible? || forced).to eq(true), check.message
|
|
146
138
|
end
|
|
147
139
|
end
|
|
148
140
|
end
|
|
@@ -6,6 +6,21 @@ module Graphiti
|
|
|
6
6
|
class SimpleErrors
|
|
7
7
|
include Enumerable
|
|
8
8
|
|
|
9
|
+
# Overridable under graphiti.errors.messages.
|
|
10
|
+
DEFAULT_MESSAGES = {
|
|
11
|
+
missing: "is missing",
|
|
12
|
+
invalid: "must be an object",
|
|
13
|
+
invalid_relationship: "is not a valid relationship",
|
|
14
|
+
unwritable_relationship: "cannot be written",
|
|
15
|
+
unknown_attribute: "is an unknown attribute",
|
|
16
|
+
unwritable_attribute: "cannot be written",
|
|
17
|
+
type_error: "should be type %{type}",
|
|
18
|
+
attribute_mismatch: "does not match the server endpoint"
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
# Joins an attribute to its message, like Rails' own errors.format.
|
|
22
|
+
DEFAULT_FORMAT = "%{attribute} %{message}"
|
|
23
|
+
|
|
9
24
|
attr_reader :messages, :details
|
|
10
25
|
|
|
11
26
|
def initialize(validation_target)
|
|
@@ -50,11 +65,9 @@ module Graphiti
|
|
|
50
65
|
end
|
|
51
66
|
alias_method :blank?, :empty?
|
|
52
67
|
|
|
53
|
-
def add(attribute, code, message: nil)
|
|
54
|
-
message ||= "is #{code.to_s.humanize.downcase}"
|
|
55
|
-
|
|
68
|
+
def add(attribute, code, message: nil, **interpolations)
|
|
56
69
|
details[attribute.to_sym] << {error: code}
|
|
57
|
-
messages[attribute.to_sym] << message
|
|
70
|
+
messages[attribute.to_sym] << translate(code, message, **interpolations, attribute: attribute)
|
|
58
71
|
end
|
|
59
72
|
|
|
60
73
|
def added?(attribute, code)
|
|
@@ -73,11 +86,19 @@ module Graphiti
|
|
|
73
86
|
|
|
74
87
|
def full_message(attribute, message)
|
|
75
88
|
return message if attribute == :base
|
|
76
|
-
|
|
89
|
+
|
|
90
|
+
translate(:format, DEFAULT_FORMAT, [:graphiti, :errors], attribute: attribute, message: message)
|
|
77
91
|
end
|
|
78
92
|
|
|
79
93
|
private
|
|
80
94
|
|
|
95
|
+
def translate(key, fallback, scope = [:graphiti, :errors, :messages], **interpolations)
|
|
96
|
+
fallback ||= DEFAULT_MESSAGES.fetch(key) { "is #{key.to_s.humanize.downcase}" }
|
|
97
|
+
return fallback % interpolations unless defined?(::I18n)
|
|
98
|
+
|
|
99
|
+
::I18n.t(key, scope: scope, default: fallback, **interpolations)
|
|
100
|
+
end
|
|
101
|
+
|
|
81
102
|
def apply_default_array(hash)
|
|
82
103
|
hash.default_proc = proc { |h, key| h[key] = [] }
|
|
83
104
|
hash
|
data/lib/graphiti/version.rb
CHANGED
data/lib/graphiti.rb
CHANGED
|
@@ -175,6 +175,7 @@ require "graphiti/audit"
|
|
|
175
175
|
require "graphiti/audit/report"
|
|
176
176
|
require "graphiti/schema"
|
|
177
177
|
require "graphiti/schema_diff"
|
|
178
|
+
require "graphiti/schema/check"
|
|
178
179
|
require "graphiti/adapters/abstract"
|
|
179
180
|
require "graphiti/resource/sideloading"
|
|
180
181
|
require "graphiti/resource/links"
|
data/lib/tasks/graphiti.rake
CHANGED
|
@@ -16,6 +16,24 @@ namespace :graphiti do
|
|
|
16
16
|
Graphiti::Debugger.flush if debug
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
namespace :schema do
|
|
20
|
+
desc "Write the schema file. Refuses backwards-incompatible changes unless FORCE_SCHEMA=true. Takes an optional path, defaulting to Graphiti.config.schema_path."
|
|
21
|
+
task :generate, [:path] => [:environment] do |_, args|
|
|
22
|
+
check = Graphiti::Schema.check(path: helpers.schema_path(args[:path]))
|
|
23
|
+
abort check.message unless check.compatible? || ENV["FORCE_SCHEMA"] == "true"
|
|
24
|
+
|
|
25
|
+
puts "Schema written: #{check.write!}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
desc "Fail unless the committed schema file exists, is up to date, and is backwards-compatible. Takes an optional path, defaulting to Graphiti.config.schema_path."
|
|
29
|
+
task :check, [:path] => [:environment] do |_, args|
|
|
30
|
+
check = Graphiti::Schema.check(path: helpers.schema_path(args[:path]))
|
|
31
|
+
abort check.message unless check.ok?
|
|
32
|
+
|
|
33
|
+
puts check.message
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
19
37
|
desc "Audit every relationship: what will raise, what loads to render ids, which render no ids, and which checks passed."
|
|
20
38
|
task audit: [:environment] do
|
|
21
39
|
helpers.setup_rails!
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: graphiti
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0.0.beta.
|
|
4
|
+
version: 2.0.0.beta.12
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lee Richmond
|
|
@@ -287,6 +287,7 @@ files:
|
|
|
287
287
|
- lib/generators/graphiti/api_test_generator.rb
|
|
288
288
|
- lib/generators/graphiti/generator_mixin.rb
|
|
289
289
|
- lib/generators/graphiti/install_generator.rb
|
|
290
|
+
- lib/generators/graphiti/locale_generator.rb
|
|
290
291
|
- lib/generators/graphiti/resource_generator.rb
|
|
291
292
|
- lib/generators/graphiti/resource_test_generator.rb
|
|
292
293
|
- lib/generators/graphiti/templates/application_resource.rb.erb
|
|
@@ -294,6 +295,7 @@ files:
|
|
|
294
295
|
- lib/generators/graphiti/templates/create_request_spec.rb.erb
|
|
295
296
|
- lib/generators/graphiti/templates/destroy_request_spec.rb.erb
|
|
296
297
|
- lib/generators/graphiti/templates/index_request_spec.rb.erb
|
|
298
|
+
- lib/generators/graphiti/templates/locale.yml.erb
|
|
297
299
|
- lib/generators/graphiti/templates/resource.rb.erb
|
|
298
300
|
- lib/generators/graphiti/templates/resource_reads_spec.rb.erb
|
|
299
301
|
- lib/generators/graphiti/templates/resource_writes_spec.rb.erb
|
|
@@ -359,6 +361,7 @@ files:
|
|
|
359
361
|
- lib/graphiti/responders.rb
|
|
360
362
|
- lib/graphiti/runner.rb
|
|
361
363
|
- lib/graphiti/schema.rb
|
|
364
|
+
- lib/graphiti/schema/check.rb
|
|
362
365
|
- lib/graphiti/schema_diff.rb
|
|
363
366
|
- lib/graphiti/scope.rb
|
|
364
367
|
- lib/graphiti/scoping/base.rb
|