graphiti 2.0.0.beta.12 → 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/CHANGELOG.md +24 -0
- data/Gemfile +3 -0
- data/lib/generators/graphiti/install_generator.rb +12 -31
- data/lib/generators/graphiti/locale_generator.rb +56 -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 +17 -9
- 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/invalid_request.rb +2 -6
- 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/railtie.rb +1 -1
- data/lib/graphiti/request_validators/update_validator.rb +1 -1
- data/lib/graphiti/resource.rb +3 -3
- data/lib/graphiti/resource_proxy.rb +14 -10
- 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/util/serializer_relationships.rb +1 -10
- data/lib/graphiti/util/transaction_hooks_recorder.rb +2 -2
- data/lib/graphiti/version.rb +1 -1
- data/lib/graphiti.rb +8 -4
- metadata +3 -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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
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
|
+
|
|
3
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)
|
|
4
28
|
|
|
5
29
|
|
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"
|
|
@@ -28,23 +28,8 @@ module Graphiti
|
|
|
28
28
|
app_controller_code
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
# In order for Graphiti to generate links, you need to set the routes host.
|
|
34
|
-
# When not explicitly set, via the HOST env var, this will fall back to
|
|
35
|
-
# the rails server settings.
|
|
36
|
-
# Rails::Server is not defined in console or rake tasks, so this will only
|
|
37
|
-
# use those defaults when they are available.
|
|
38
|
-
routes.default_url_options[:host] = ENV.fetch('HOST') do
|
|
39
|
-
if defined?(Rails::Server)
|
|
40
|
-
argv_options = Rails::Server::Options.new.parse!(ARGV)
|
|
41
|
-
"http://#{argv_options[:Host]}:#{argv_options[:Port]}"
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
RUBY
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
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"))
|
|
48
33
|
inject_into_file "spec/rails_helper.rb", after: /RSpec.configure.+^end$/m do
|
|
49
34
|
<<~RUBY
|
|
50
35
|
|
|
@@ -60,20 +45,7 @@ module Graphiti
|
|
|
60
45
|
end
|
|
61
46
|
|
|
62
47
|
insert_into_file "config/routes.rb", after: "Rails.application.routes.draw do\n" do
|
|
63
|
-
|
|
64
|
-
<<-STR
|
|
65
|
-
scope path: ApplicationResource.endpoint_namespace, defaults: { format: :jsonapi } do
|
|
66
|
-
mount VandalUi::Engine, at: '/vandal'
|
|
67
|
-
# your routes go here
|
|
68
|
-
end
|
|
69
|
-
STR
|
|
70
|
-
else
|
|
71
|
-
<<-STR
|
|
72
|
-
scope path: ApplicationResource.endpoint_namespace, defaults: { format: :jsonapi } do
|
|
73
|
-
# your routes go here
|
|
74
|
-
end
|
|
75
|
-
STR
|
|
76
|
-
end
|
|
48
|
+
namespace_scope
|
|
77
49
|
end
|
|
78
50
|
end
|
|
79
51
|
|
|
@@ -83,6 +55,15 @@ module Graphiti
|
|
|
83
55
|
@options["omit-comments"]
|
|
84
56
|
end
|
|
85
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
|
+
|
|
86
67
|
def app_controller_code
|
|
87
68
|
str = +" include Graphiti::Rails::Controller\n"
|
|
88
69
|
if defined?(::Responders)
|
|
@@ -8,6 +8,28 @@ module Graphiti
|
|
|
8
8
|
aliases: ["-c"],
|
|
9
9
|
desc: "Generate without documentation comments"
|
|
10
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
|
+
|
|
11
33
|
desc "Writes the error text Graphiti renders, ready to edit"
|
|
12
34
|
def locale
|
|
13
35
|
template("locale.yml.erb", File.join("config/locales", "graphiti.en.yml"))
|
|
@@ -19,6 +41,40 @@ module Graphiti
|
|
|
19
41
|
@options["omit-comments"]
|
|
20
42
|
end
|
|
21
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
|
+
|
|
22
78
|
def default_messages
|
|
23
79
|
Graphiti::Util::SimpleErrors::DEFAULT_MESSAGES
|
|
24
80
|
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 %>
|
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
<%- unless omit_comments? -%>
|
|
2
|
-
# Everything Graphiti puts in an errors payload.
|
|
3
2
|
# For another language, add a block below or a graphiti.<locale>.yml file.
|
|
4
3
|
<%- end -%>
|
|
5
4
|
en:
|
|
6
5
|
graphiti:
|
|
7
6
|
errors:
|
|
8
7
|
<%- unless omit_comments? -%>
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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".
|
|
17
25
|
<%- end -%>
|
|
18
26
|
format: <%= default_format.inspect %>
|
|
19
27
|
messages:
|
|
@@ -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,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,12 +50,6 @@ module Graphiti
|
|
|
48
50
|
"bad_request"
|
|
49
51
|
end
|
|
50
52
|
|
|
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
53
|
def default_title
|
|
58
54
|
"Request Error"
|
|
59
55
|
end
|
|
@@ -3,6 +3,8 @@ module Graphiti
|
|
|
3
3
|
# Walks into related objects so nested writes report against the record
|
|
4
4
|
# that actually failed.
|
|
5
5
|
class Validation
|
|
6
|
+
include TranslatedTitle
|
|
7
|
+
|
|
6
8
|
attr_reader :object
|
|
7
9
|
|
|
8
10
|
def initialize(object, relationship_payloads = {}, relationship_meta = {})
|
|
@@ -13,14 +15,14 @@ module Graphiti
|
|
|
13
15
|
|
|
14
16
|
def attribute_errors
|
|
15
17
|
[].tap do |errors|
|
|
16
|
-
each_error do |attribute, message,
|
|
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
|
data/lib/graphiti/resource.rb
CHANGED
|
@@ -37,8 +37,8 @@ module Graphiti
|
|
|
37
37
|
end
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
def with_context(object,
|
|
41
|
-
Graphiti.with_context(object,
|
|
40
|
+
def with_context(object, action = nil)
|
|
41
|
+
Graphiti.with_context(object, action) do
|
|
42
42
|
yield
|
|
43
43
|
end
|
|
44
44
|
end
|
|
@@ -55,7 +55,7 @@ module Graphiti
|
|
|
55
55
|
# is called, not a fixed list. Persistence overrides it with :create/:update
|
|
56
56
|
# while saving, and :show while resolving sideloads afterwards.
|
|
57
57
|
def self.current_action
|
|
58
|
-
Graphiti.context[:
|
|
58
|
+
Graphiti.context[:action]
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
def current_action
|
|
@@ -159,7 +159,7 @@ module Graphiti
|
|
|
159
159
|
params = normalized_params_copy(params)
|
|
160
160
|
add_endpoint_filter(params, action)
|
|
161
161
|
validator = ::Graphiti::RequestValidator.new(@resource, params, action)
|
|
162
|
-
validator.validate!
|
|
162
|
+
with_context_action(action) { validator.validate! }
|
|
163
163
|
|
|
164
164
|
if @assigned_model && same_write_payload?(validator.deserialized_payload)
|
|
165
165
|
return @assigned_model
|
|
@@ -174,32 +174,36 @@ module Graphiti
|
|
|
174
174
|
)
|
|
175
175
|
end
|
|
176
176
|
|
|
177
|
+
# TODO: remove this. Only used for persisting many-to-many with AR
|
|
178
|
+
# (see activerecord adapter)
|
|
179
|
+
def with_context_action(action)
|
|
180
|
+
original = Graphiti.context[:action]
|
|
181
|
+
Graphiti.context[:action] = action
|
|
182
|
+
yield
|
|
183
|
+
ensure
|
|
184
|
+
Graphiti.context[:action] = original
|
|
185
|
+
end
|
|
186
|
+
|
|
177
187
|
def save(action: :create)
|
|
178
|
-
|
|
179
|
-
# (see activerecord adapter)
|
|
180
|
-
original = Graphiti.context[:namespace]
|
|
181
|
-
begin
|
|
182
|
-
Graphiti.context[:namespace] = action
|
|
188
|
+
validator = with_context_action(action) do
|
|
183
189
|
# An assigned model can only come from #assign_attributes, which
|
|
184
190
|
# validated the payload it stored - re-validating here would run the
|
|
185
191
|
# writable guards (and their guard_model lookups) a redundant time.
|
|
186
192
|
unless @assigned_model
|
|
187
193
|
::Graphiti::RequestValidator.new(@resource, @payload.params, action).validate!
|
|
188
194
|
end
|
|
189
|
-
|
|
195
|
+
persist {
|
|
190
196
|
@resource.persist_with_relationships \
|
|
191
197
|
@payload.meta(action: action),
|
|
192
198
|
@payload.attributes,
|
|
193
199
|
@payload.relationships,
|
|
194
200
|
assigned_model: @assigned_model
|
|
195
201
|
}
|
|
196
|
-
ensure
|
|
197
|
-
Graphiti.context[:namespace] = original
|
|
198
202
|
end
|
|
199
203
|
@data, success = validator.to_a
|
|
200
204
|
|
|
201
205
|
if success
|
|
202
|
-
# If the context
|
|
206
|
+
# If the context action is `update` or `create`, certain
|
|
203
207
|
# adapters will cause N+1 validation calls, so lets explicitly
|
|
204
208
|
# switch to a lookup context.
|
|
205
209
|
Graphiti.with_context(Graphiti.context[:object], :show) do
|
data/lib/graphiti/scope.rb
CHANGED
|
@@ -39,18 +39,17 @@ module Graphiti
|
|
|
39
39
|
!Graphiti.config.concurrency || on_pool_thread?
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
# TODO: move to Fiber[] once the floor is Ruby 3.2
|
|
43
42
|
def self.on_pool_thread?
|
|
44
|
-
|
|
43
|
+
Fiber[POOL_THREAD] == true
|
|
45
44
|
end
|
|
46
45
|
|
|
47
46
|
# Restores rather than clears because :caller_runs may have run the task on a request thread.
|
|
48
47
|
def self.marking_pool_thread
|
|
49
|
-
previous =
|
|
50
|
-
|
|
48
|
+
previous = Fiber[POOL_THREAD]
|
|
49
|
+
Fiber[POOL_THREAD] = true
|
|
51
50
|
yield
|
|
52
51
|
ensure
|
|
53
|
-
|
|
52
|
+
Fiber[POOL_THREAD] = previous
|
|
54
53
|
end
|
|
55
54
|
|
|
56
55
|
def initialize(object, resource, query, opts = {})
|
data/lib/graphiti/sideload.rb
CHANGED
|
@@ -61,6 +61,27 @@ module Graphiti
|
|
|
61
61
|
self.scope_proc = blk
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
+
ASSIGNING_QUERY = :__graphiti_assigning_query
|
|
65
|
+
private_constant :ASSIGNING_QUERY
|
|
66
|
+
|
|
67
|
+
def self.assigning_node(query)
|
|
68
|
+
return yield if query.nil?
|
|
69
|
+
|
|
70
|
+
previous = Fiber[ASSIGNING_QUERY]
|
|
71
|
+
Fiber[ASSIGNING_QUERY] = query
|
|
72
|
+
yield
|
|
73
|
+
ensure
|
|
74
|
+
Fiber[ASSIGNING_QUERY] = previous unless query.nil?
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.current_assigning_query
|
|
78
|
+
Fiber[ASSIGNING_QUERY]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.assigned?(query, record, association_name)
|
|
82
|
+
query.association_owners.key?([record.object_id, association_name])
|
|
83
|
+
end
|
|
84
|
+
|
|
64
85
|
def self.assign(&blk)
|
|
65
86
|
self.assign_proc = blk
|
|
66
87
|
end
|
|
@@ -365,7 +386,7 @@ module Graphiti
|
|
|
365
386
|
|
|
366
387
|
if self.class.scope_proc
|
|
367
388
|
build_sideload_scope(parents, query, graph_parent).resolve do |sideload_results|
|
|
368
|
-
fire_assign(parents, sideload_results)
|
|
389
|
+
fire_assign(parents, sideload_results, query)
|
|
369
390
|
end
|
|
370
391
|
else
|
|
371
392
|
sync_load(parents, query, graph_parent, &proxy_block)
|
|
@@ -378,7 +399,7 @@ module Graphiti
|
|
|
378
399
|
|
|
379
400
|
if self.class.scope_proc
|
|
380
401
|
build_sideload_scope(parents, query, graph_parent).future_resolve do |sideload_results|
|
|
381
|
-
fire_assign(parents, sideload_results)
|
|
402
|
+
fire_assign(parents, sideload_results, query)
|
|
382
403
|
end
|
|
383
404
|
else
|
|
384
405
|
future_load(parents, query, graph_parent, &proxy_block)
|
|
@@ -413,10 +434,14 @@ module Graphiti
|
|
|
413
434
|
end
|
|
414
435
|
|
|
415
436
|
def associate_all(parent, children)
|
|
437
|
+
return unless claim_association(parent)
|
|
438
|
+
|
|
416
439
|
parent_resource.associate_all(parent, children, association_name, type)
|
|
417
440
|
end
|
|
418
441
|
|
|
419
442
|
def associate(parent, child)
|
|
443
|
+
return unless claim_association(parent)
|
|
444
|
+
|
|
420
445
|
parent_resource.associate(parent, child, association_name, type)
|
|
421
446
|
end
|
|
422
447
|
|
|
@@ -506,7 +531,7 @@ module Graphiti
|
|
|
506
531
|
opts[:sideload_parent_length] = parents.length
|
|
507
532
|
opts[:query] = query
|
|
508
533
|
opts[:after_resolve] = ->(results) {
|
|
509
|
-
fire_assign(parents, results)
|
|
534
|
+
fire_assign(parents, results, query)
|
|
510
535
|
}
|
|
511
536
|
end
|
|
512
537
|
end
|
|
@@ -519,16 +544,28 @@ module Graphiti
|
|
|
519
544
|
end
|
|
520
545
|
end
|
|
521
546
|
|
|
522
|
-
def fire_assign(parents, children)
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
547
|
+
def fire_assign(parents, children, query = nil)
|
|
548
|
+
self.class.assigning_node(query) do
|
|
549
|
+
with_error_handling Errors::SideloadAssignError do
|
|
550
|
+
if self.class.assign_proc
|
|
551
|
+
instance_exec(parents, children, &self.class.assign_proc)
|
|
552
|
+
else
|
|
553
|
+
assign(parents, children)
|
|
554
|
+
end
|
|
528
555
|
end
|
|
529
556
|
end
|
|
530
557
|
end
|
|
531
558
|
|
|
559
|
+
# A record can be shared by two nodes of the include tree, and a node that narrows
|
|
560
|
+
# differently returns different rows, so the first node to populate an association owns it.
|
|
561
|
+
def claim_association(parent)
|
|
562
|
+
query = self.class.current_assigning_query
|
|
563
|
+
return true if query.nil?
|
|
564
|
+
|
|
565
|
+
key = [parent.object_id, association_name]
|
|
566
|
+
query.association_owners.compute_if_absent(key) { query.hash } == query.hash
|
|
567
|
+
end
|
|
568
|
+
|
|
532
569
|
def with_error_handling(error_class)
|
|
533
570
|
begin
|
|
534
571
|
result = yield
|
|
@@ -59,6 +59,7 @@ module Graphiti
|
|
|
59
59
|
|
|
60
60
|
class RelationMatcher < BaseMatcher
|
|
61
61
|
GRAPHITI_OPTS = %i[primary_key foreign_key resource readable writable link single].freeze
|
|
62
|
+
SIDELOAD_METHODS = {resource: :resource_class, readable: :readable?, writable: :writable?, single: :single?}.freeze
|
|
62
63
|
GRAPHITI_CONFIG_KEY = :sideloads
|
|
63
64
|
EXPECTED_ACTION = ""
|
|
64
65
|
|
|
@@ -73,7 +74,7 @@ module Graphiti
|
|
|
73
74
|
attr_reader :target, :opts, :resource
|
|
74
75
|
|
|
75
76
|
def assert_opt(opt)
|
|
76
|
-
asserted_opt = (opt
|
|
77
|
+
asserted_opt = SIDELOAD_METHODS.fetch(opt, opt)
|
|
77
78
|
return true if config.send(asserted_opt) == opts[opt]
|
|
78
79
|
|
|
79
80
|
@opt_failures << opt_failure_message(opt, opts[opt], config.send(asserted_opt))
|
|
@@ -71,7 +71,7 @@ module Graphiti
|
|
|
71
71
|
# not predict, so the loaded records win. Only the un-included case
|
|
72
72
|
# is worth short-circuiting.
|
|
73
73
|
if sideload_ref.resource_ids_from_foreign_key? &&
|
|
74
|
-
|
|
74
|
+
!::Graphiti::Sideload.assigned?(@proxy.query, @object, sideload_ref.association_name)
|
|
75
75
|
linkage always: sideload_ref.render_resource_ids? do
|
|
76
76
|
foreign_key = begin
|
|
77
77
|
@object.public_send(sideload_ref.foreign_key)
|
|
@@ -104,15 +104,6 @@ module Graphiti
|
|
|
104
104
|
end
|
|
105
105
|
end
|
|
106
106
|
|
|
107
|
-
# A relationship nested under another one is still loaded and can still
|
|
108
|
-
# be narrowed by a deep filter, so the foreign key is not a safe
|
|
109
|
-
# stand-in for what the request actually returns.
|
|
110
|
-
def included_anywhere?(include_hash, name)
|
|
111
|
-
include_hash.any? do |key, nested|
|
|
112
|
-
key == name || included_anywhere?(nested, name)
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
|
|
116
107
|
def data_proc
|
|
117
108
|
sideload_ref = @sideload
|
|
118
109
|
resource_class_ref = @resource_class
|
|
@@ -58,11 +58,11 @@ module Graphiti
|
|
|
58
58
|
private
|
|
59
59
|
|
|
60
60
|
def _hooks
|
|
61
|
-
|
|
61
|
+
Fiber[:_graphiti_hooks]
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
def reset_hooks
|
|
65
|
-
|
|
65
|
+
Fiber[:_graphiti_hooks] = {
|
|
66
66
|
after_graph_persist: [],
|
|
67
67
|
before_commit: [],
|
|
68
68
|
after_commit: []
|
data/lib/graphiti/version.rb
CHANGED
data/lib/graphiti.rb
CHANGED
|
@@ -51,18 +51,20 @@ module Graphiti
|
|
|
51
51
|
|
|
52
52
|
# @api private
|
|
53
53
|
def self.context
|
|
54
|
-
|
|
54
|
+
Fiber[:context] ||= ContextHash.new
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
# @api private
|
|
58
58
|
def self.context=(val)
|
|
59
|
-
|
|
59
|
+
Fiber[:context] = ContextHash.new.tap do |hash|
|
|
60
|
+
val.each { |key, value| hash[key] = value }
|
|
61
|
+
end
|
|
60
62
|
end
|
|
61
63
|
|
|
62
64
|
# @api private
|
|
63
|
-
def self.with_context(obj,
|
|
65
|
+
def self.with_context(obj, action = nil)
|
|
64
66
|
prior = context
|
|
65
|
-
self.context = {object: obj,
|
|
67
|
+
self.context = {object: obj, action: action}
|
|
66
68
|
yield
|
|
67
69
|
ensure
|
|
68
70
|
self.context = prior
|
|
@@ -169,6 +171,7 @@ require "graphiti/version"
|
|
|
169
171
|
require "graphiti/jsonapi_serializable_ext"
|
|
170
172
|
require "graphiti/configuration"
|
|
171
173
|
require "graphiti/context"
|
|
174
|
+
require "graphiti/context_hash"
|
|
172
175
|
require "graphiti/errors"
|
|
173
176
|
require "graphiti/types"
|
|
174
177
|
require "graphiti/audit"
|
|
@@ -239,6 +242,7 @@ require "graphiti/query"
|
|
|
239
242
|
require "graphiti/debugger"
|
|
240
243
|
require "graphiti/util/cache_debug"
|
|
241
244
|
require "graphiti/util/uri_decoder"
|
|
245
|
+
require "graphiti/error_serializers/translated_title"
|
|
242
246
|
require "graphiti/error_serializers/validation"
|
|
243
247
|
require "graphiti/error_serializers/invalid_request"
|
|
244
248
|
require "graphiti/error_serializers/conflict_request"
|
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.13
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lee Richmond
|
|
@@ -318,12 +318,14 @@ files:
|
|
|
318
318
|
- lib/graphiti/cli.rb
|
|
319
319
|
- lib/graphiti/configuration.rb
|
|
320
320
|
- lib/graphiti/context.rb
|
|
321
|
+
- lib/graphiti/context_hash.rb
|
|
321
322
|
- lib/graphiti/debugger.rb
|
|
322
323
|
- lib/graphiti/delegates/pagination.rb
|
|
323
324
|
- lib/graphiti/deserializer.rb
|
|
324
325
|
- lib/graphiti/error_serializers/conflict_request.rb
|
|
325
326
|
- lib/graphiti/error_serializers/deprecated_constants.rb
|
|
326
327
|
- lib/graphiti/error_serializers/invalid_request.rb
|
|
328
|
+
- lib/graphiti/error_serializers/translated_title.rb
|
|
327
329
|
- lib/graphiti/error_serializers/validation.rb
|
|
328
330
|
- lib/graphiti/errors.rb
|
|
329
331
|
- lib/graphiti/extensions/boolean_attribute.rb
|