graphiti 2.0.0.beta.11 → 2.0.0.beta.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +6 -0
- data/CHANGELOG.md +39 -0
- data/Gemfile +3 -0
- data/lib/generators/graphiti/generator_mixin.rb +5 -4
- data/lib/generators/graphiti/install_generator.rb +15 -31
- data/lib/generators/graphiti/locale_generator.rb +86 -0
- data/lib/generators/graphiti/resource_generator.rb +2 -1
- data/lib/generators/graphiti/templates/application_resource.rb.erb +6 -9
- data/lib/generators/graphiti/templates/controller.rb.erb +1 -1
- data/lib/generators/graphiti/templates/locale.yml.erb +30 -0
- data/lib/generators/graphiti/templates/resource_writes_spec.rb.erb +1 -1
- data/lib/graphiti/adapters/active_record.rb +1 -1
- data/lib/graphiti/context_hash.rb +21 -0
- data/lib/graphiti/debugger.rb +3 -3
- data/lib/graphiti/error_serializers/conflict_request.rb +1 -1
- data/lib/graphiti/error_serializers/deprecated_constants.rb +6 -16
- data/lib/graphiti/error_serializers/invalid_request.rb +3 -1
- data/lib/graphiti/error_serializers/translated_title.rb +13 -0
- data/lib/graphiti/error_serializers/validation.rb +14 -4
- data/lib/graphiti/query.rb +8 -1
- data/lib/graphiti/rails/exception_handlers.rb +17 -0
- data/lib/graphiti/rails/railtie.rb +11 -1
- data/lib/graphiti/rails/rake_helpers.rb +5 -0
- data/lib/graphiti/rails.rb +18 -0
- data/lib/graphiti/request_validators/update_validator.rb +2 -6
- data/lib/graphiti/request_validators/validator.rb +6 -6
- data/lib/graphiti/resource.rb +3 -3
- data/lib/graphiti/resource_proxy.rb +14 -10
- data/lib/graphiti/schema/check.rb +69 -0
- data/lib/graphiti/schema.rb +8 -9
- data/lib/graphiti/scope.rb +4 -5
- data/lib/graphiti/sideload.rb +46 -9
- data/lib/graphiti/spec_helpers/matchers.rb +2 -1
- data/lib/graphiti/spec_helpers/rspec.rb +5 -13
- data/lib/graphiti/util/serializer_relationships.rb +1 -10
- data/lib/graphiti/util/simple_errors.rb +26 -5
- data/lib/graphiti/util/transaction_hooks_recorder.rb +2 -2
- data/lib/graphiti/version.rb +1 -1
- data/lib/graphiti.rb +9 -4
- data/lib/tasks/graphiti.rake +18 -0
- metadata +6 -1
|
@@ -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?
|
|
@@ -32,7 +28,7 @@ module Graphiti
|
|
|
32
28
|
end
|
|
33
29
|
|
|
34
30
|
def payload_matches_endpoint?
|
|
35
|
-
unless @params.dig(:data, :id) == @params.dig(:filter, :id)
|
|
31
|
+
unless @params.dig(:data, :id).to_s == @params.dig(:filter, :id).to_s
|
|
36
32
|
attribute_mismatch([:data, :id])
|
|
37
33
|
end
|
|
38
34
|
|
|
@@ -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
|
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
|
|
@@ -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?)
|
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))
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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,12 +171,14 @@ 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"
|
|
175
178
|
require "graphiti/audit/report"
|
|
176
179
|
require "graphiti/schema"
|
|
177
180
|
require "graphiti/schema_diff"
|
|
181
|
+
require "graphiti/schema/check"
|
|
178
182
|
require "graphiti/adapters/abstract"
|
|
179
183
|
require "graphiti/resource/sideloading"
|
|
180
184
|
require "graphiti/resource/links"
|
|
@@ -238,6 +242,7 @@ require "graphiti/query"
|
|
|
238
242
|
require "graphiti/debugger"
|
|
239
243
|
require "graphiti/util/cache_debug"
|
|
240
244
|
require "graphiti/util/uri_decoder"
|
|
245
|
+
require "graphiti/error_serializers/translated_title"
|
|
241
246
|
require "graphiti/error_serializers/validation"
|
|
242
247
|
require "graphiti/error_serializers/invalid_request"
|
|
243
248
|
require "graphiti/error_serializers/conflict_request"
|
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.13
|
|
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
|
|
@@ -316,12 +318,14 @@ files:
|
|
|
316
318
|
- lib/graphiti/cli.rb
|
|
317
319
|
- lib/graphiti/configuration.rb
|
|
318
320
|
- lib/graphiti/context.rb
|
|
321
|
+
- lib/graphiti/context_hash.rb
|
|
319
322
|
- lib/graphiti/debugger.rb
|
|
320
323
|
- lib/graphiti/delegates/pagination.rb
|
|
321
324
|
- lib/graphiti/deserializer.rb
|
|
322
325
|
- lib/graphiti/error_serializers/conflict_request.rb
|
|
323
326
|
- lib/graphiti/error_serializers/deprecated_constants.rb
|
|
324
327
|
- lib/graphiti/error_serializers/invalid_request.rb
|
|
328
|
+
- lib/graphiti/error_serializers/translated_title.rb
|
|
325
329
|
- lib/graphiti/error_serializers/validation.rb
|
|
326
330
|
- lib/graphiti/errors.rb
|
|
327
331
|
- lib/graphiti/extensions/boolean_attribute.rb
|
|
@@ -359,6 +363,7 @@ files:
|
|
|
359
363
|
- lib/graphiti/responders.rb
|
|
360
364
|
- lib/graphiti/runner.rb
|
|
361
365
|
- lib/graphiti/schema.rb
|
|
366
|
+
- lib/graphiti/schema/check.rb
|
|
362
367
|
- lib/graphiti/schema_diff.rb
|
|
363
368
|
- lib/graphiti/scope.rb
|
|
364
369
|
- lib/graphiti/scoping/base.rb
|