graphiti 2.0.0.beta.5 → 2.0.0.beta.7
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 +5 -0
- data/CHANGELOG.md +24 -0
- data/docs/concepts/relationships.md +109 -9
- data/docs/topics/debugging.md +26 -0
- data/docs/topics/testing.md +44 -22
- data/docs/upgrading.md +15 -8
- data/graphiti.gemspec +1 -1
- data/lib/generators/graphiti/install_generator.rb +0 -1
- data/lib/generators/graphiti/templates/index_request_spec.rb.erb +2 -2
- data/lib/generators/graphiti/templates/resource_reads_spec.rb.erb +4 -4
- data/lib/generators/graphiti/templates/show_request_spec.rb.erb +2 -2
- data/lib/graphiti/audit/report.rb +226 -0
- data/lib/graphiti/audit.rb +231 -0
- data/lib/graphiti/errors.rb +47 -0
- data/lib/graphiti/query.rb +24 -5
- data/lib/graphiti/request_validators/validator.rb +10 -1
- data/lib/graphiti/resource/configuration.rb +16 -1
- data/lib/graphiti/resource/interface.rb +3 -3
- data/lib/graphiti/resource/polymorphism.rb +10 -0
- data/lib/graphiti/runner.rb +8 -3
- data/lib/graphiti/schema.rb +1 -1
- data/lib/graphiti/scope.rb +13 -0
- data/lib/graphiti/sideload/belongs_to.rb +22 -29
- data/lib/graphiti/sideload.rb +48 -13
- data/lib/graphiti/spec_helpers.rb +5 -0
- data/lib/graphiti/util/serializer_relationships.rb +27 -4
- data/lib/graphiti/version.rb +1 -1
- data/lib/graphiti.rb +2 -0
- data/lib/tasks/graphiti.rake +9 -0
- data/package.json +1 -0
- data/website/sidebars.js +4 -4
- metadata +3 -1
|
@@ -8,6 +8,8 @@ module Graphiti
|
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def serializer_for(model)
|
|
11
|
+
return super unless self.class.polymorphic?
|
|
12
|
+
|
|
11
13
|
if polymorphic_child?
|
|
12
14
|
serializer
|
|
13
15
|
else
|
|
@@ -17,10 +19,14 @@ module Graphiti
|
|
|
17
19
|
end
|
|
18
20
|
|
|
19
21
|
def associate_all(*args)
|
|
22
|
+
return super unless self.class.polymorphic?
|
|
23
|
+
|
|
20
24
|
_associate(:associate_all, *args)
|
|
21
25
|
end
|
|
22
26
|
|
|
23
27
|
def associate(*args)
|
|
28
|
+
return super unless self.class.polymorphic?
|
|
29
|
+
|
|
24
30
|
_associate(:associate, *args)
|
|
25
31
|
end
|
|
26
32
|
|
|
@@ -34,6 +40,8 @@ module Graphiti
|
|
|
34
40
|
|
|
35
41
|
module ClassMethods
|
|
36
42
|
def inherited(klass)
|
|
43
|
+
return super unless polymorphic?
|
|
44
|
+
|
|
37
45
|
klass.type = nil
|
|
38
46
|
klass.model = klass.infer_model
|
|
39
47
|
klass.endpoint = klass.infer_endpoint
|
|
@@ -42,6 +50,8 @@ module Graphiti
|
|
|
42
50
|
end
|
|
43
51
|
|
|
44
52
|
def sideload(name)
|
|
53
|
+
return super unless polymorphic?
|
|
54
|
+
|
|
45
55
|
if (split_on = name.to_s.split(/^on__/)).length > 1
|
|
46
56
|
on_type, name = split_on[1].split("--").map(&:to_sym)
|
|
47
57
|
end
|
data/lib/graphiti/runner.rb
CHANGED
|
@@ -3,8 +3,13 @@ module Graphiti
|
|
|
3
3
|
attr_reader :params
|
|
4
4
|
attr_reader :deserialized_payload
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
def initialize(resource_class, params, *positional, query: nil, action: nil)
|
|
7
|
+
if positional.any?
|
|
8
|
+
Graphiti::DEPRECATOR.warn("Passing query/action to Runner.new positionally is deprecated. Use query:/action: keywords.")
|
|
9
|
+
query ||= positional[0]
|
|
10
|
+
action ||= positional[1]
|
|
11
|
+
end
|
|
12
|
+
|
|
8
13
|
@resource_class = resource_class
|
|
9
14
|
@params = params
|
|
10
15
|
@query = query
|
|
@@ -30,7 +35,7 @@ module Graphiti
|
|
|
30
35
|
end
|
|
31
36
|
|
|
32
37
|
def query
|
|
33
|
-
@query ||= Query.new(jsonapi_resource, params,
|
|
38
|
+
@query ||= Query.new(jsonapi_resource, params, action: @action)
|
|
34
39
|
end
|
|
35
40
|
|
|
36
41
|
def query_hash
|
data/lib/graphiti/schema.rb
CHANGED
data/lib/graphiti/scope.rb
CHANGED
|
@@ -154,12 +154,25 @@ module Graphiti
|
|
|
154
154
|
payload[:results]
|
|
155
155
|
}
|
|
156
156
|
resolved.compact!
|
|
157
|
+
deduplicate_entities!(resolved)
|
|
157
158
|
assign_serializer(resolved)
|
|
158
159
|
yield resolved if block_given?
|
|
159
160
|
@opts[:after_resolve]&.call(resolved)
|
|
160
161
|
resolved
|
|
161
162
|
end
|
|
162
163
|
|
|
164
|
+
# Must run before sideloads assign, so every include path populates the
|
|
165
|
+
# canonical instance. The resource class in the key keeps two resources
|
|
166
|
+
# serving the same model from sharing an instance and a serializer.
|
|
167
|
+
def deduplicate_entities!(resolved)
|
|
168
|
+
resolved.map! do |record|
|
|
169
|
+
next record unless record.respond_to?(:id) && !record.id.nil?
|
|
170
|
+
|
|
171
|
+
key = [@resource.class, record.class, record.id]
|
|
172
|
+
@query.entity_map.compute_if_absent(key) { record }
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
163
176
|
def each_applicable_sideload
|
|
164
177
|
@query.sideloads.each_pair do |name, sideload_query|
|
|
165
178
|
sideload = @resource.class.sideload(name)
|
|
@@ -3,40 +3,33 @@ class Graphiti::Sideload::BelongsTo < Graphiti::Sideload
|
|
|
3
3
|
:belongs_to
|
|
4
4
|
end
|
|
5
5
|
|
|
6
|
-
def
|
|
7
|
-
|
|
6
|
+
def default_render_resource_ids?
|
|
7
|
+
case parent_resource_class&.belongs_to_resource_ids_by_default
|
|
8
|
+
when :always then renderable_at_all?
|
|
9
|
+
when :never then false
|
|
10
|
+
else resource_ids_from_foreign_key?
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def renderable_at_all?
|
|
15
|
+
readable_guarded? || readable?
|
|
8
16
|
end
|
|
9
17
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
# relationship, so the key alone cannot say what type the id has
|
|
20
|
-
# - a remote resource has no local foreign key to read
|
|
21
|
-
# - a custom primary_key points the relationship at some other column, so
|
|
22
|
-
# the key holds that column's value rather than the related id
|
|
23
|
-
def linkage_from_foreign_key?
|
|
24
|
-
# Ask before resolving #resource: an unreadable relationship renders
|
|
25
|
-
# nothing, and its resource class may not even be inferrable.
|
|
26
|
-
return false unless readable?
|
|
27
|
-
return false unless foreign_key_is_related_id?
|
|
28
|
-
return false if polymorphic_child?
|
|
29
|
-
return false if self.class.scope_proc || self.class.params_proc
|
|
30
|
-
return false if @base_scope
|
|
31
|
-
return false if remote?
|
|
32
|
-
return false if resource.class.polymorphic.present?
|
|
18
|
+
def resource_ids_blocker
|
|
19
|
+
return :unreadable unless renderable_at_all?
|
|
20
|
+
return :custom_primary_key unless foreign_key_is_related_id?
|
|
21
|
+
return :polymorphic_child if polymorphic_child?
|
|
22
|
+
return :scope_block if self.class.scope_proc
|
|
23
|
+
return :params_block if self.class.params_proc
|
|
24
|
+
return :base_scope if @base_scope
|
|
25
|
+
return :remote if remote?
|
|
26
|
+
return :polymorphic_resource if resource.class.polymorphic.present?
|
|
33
27
|
|
|
34
|
-
|
|
28
|
+
nil
|
|
35
29
|
end
|
|
36
30
|
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
# that at another column means the key holds that column instead.
|
|
31
|
+
# base_filter matches the foreign key against primary_key, so a custom
|
|
32
|
+
# primary_key means the key holds that column's value, not the related id.
|
|
40
33
|
def foreign_key_is_related_id?
|
|
41
34
|
primary_key == :id
|
|
42
35
|
end
|
data/lib/graphiti/sideload.rb
CHANGED
|
@@ -20,6 +20,7 @@ module Graphiti
|
|
|
20
20
|
def initialize(name, opts)
|
|
21
21
|
@name = name
|
|
22
22
|
validate_options!(opts)
|
|
23
|
+
translate_deprecated_options!(opts)
|
|
23
24
|
@parent_resource_class = opts[:parent_resource]
|
|
24
25
|
@resource_class_name = opts[:resource]
|
|
25
26
|
@primary_key = opts[:primary_key]
|
|
@@ -42,7 +43,7 @@ module Graphiti
|
|
|
42
43
|
@group_name = opts[:group_name]
|
|
43
44
|
@polymorphic_child = opts[:polymorphic_child]
|
|
44
45
|
@parent = opts[:parent]
|
|
45
|
-
@
|
|
46
|
+
@render_resource_ids = opts[:resource_ids]
|
|
46
47
|
|
|
47
48
|
if polymorphic_child?
|
|
48
49
|
parent.resource.polymorphic << resource_class
|
|
@@ -110,6 +111,31 @@ module Graphiti
|
|
|
110
111
|
dynamic_flag?(@readable) || dynamic_flag?(@writable)
|
|
111
112
|
end
|
|
112
113
|
|
|
114
|
+
def readable_guarded?
|
|
115
|
+
dynamic_flag?(@readable)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def readable_guard_name
|
|
119
|
+
@readable.to_sym if @readable.is_a?(Symbol) || @readable.is_a?(String)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def non_default_options
|
|
123
|
+
options = {}
|
|
124
|
+
options[:as] = association_name if @as
|
|
125
|
+
options[:primary_key] = primary_key unless primary_key == :id
|
|
126
|
+
options[:single] = true if single?
|
|
127
|
+
options[:remote] = @remote if remote?
|
|
128
|
+
options[:link] = @link unless @link.nil?
|
|
129
|
+
options[:readable] = @readable unless @readable.nil? || @readable == true
|
|
130
|
+
options[:writable] = @writable unless @writable.nil? || @writable == true
|
|
131
|
+
options[:resource_ids] = @render_resource_ids unless @render_resource_ids.nil?
|
|
132
|
+
options
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def customized_base_scope?
|
|
136
|
+
!!@base_scope
|
|
137
|
+
end
|
|
138
|
+
|
|
113
139
|
def single?
|
|
114
140
|
!!@single
|
|
115
141
|
end
|
|
@@ -122,24 +148,21 @@ module Graphiti
|
|
|
122
148
|
!!@polymorphic_as
|
|
123
149
|
end
|
|
124
150
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
def linkage_from_foreign_key?
|
|
128
|
-
false
|
|
151
|
+
def resource_ids_from_foreign_key?
|
|
152
|
+
resource_ids_blocker.nil?
|
|
129
153
|
end
|
|
130
154
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return !!@always_include_resource_ids unless @always_include_resource_ids.nil?
|
|
155
|
+
def resource_ids_blocker
|
|
156
|
+
:no_foreign_key_on_parent
|
|
157
|
+
end
|
|
135
158
|
|
|
136
|
-
|
|
137
|
-
return
|
|
159
|
+
def render_resource_ids?
|
|
160
|
+
return !!@render_resource_ids unless @render_resource_ids.nil?
|
|
138
161
|
|
|
139
|
-
|
|
162
|
+
default_render_resource_ids?
|
|
140
163
|
end
|
|
141
164
|
|
|
142
|
-
def
|
|
165
|
+
def default_render_resource_ids?
|
|
143
166
|
false
|
|
144
167
|
end
|
|
145
168
|
|
|
@@ -434,6 +457,18 @@ module Graphiti
|
|
|
434
457
|
false
|
|
435
458
|
end
|
|
436
459
|
|
|
460
|
+
def translate_deprecated_options!(opts)
|
|
461
|
+
return unless opts.key?(:always_include_resource_ids)
|
|
462
|
+
|
|
463
|
+
Graphiti::DEPRECATOR.deprecation_warning(
|
|
464
|
+
:always_include_resource_ids,
|
|
465
|
+
"Use :resource_ids instead (#{opts[:parent_resource]&.name}##{@name})"
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
value = opts.delete(:always_include_resource_ids)
|
|
469
|
+
opts[:resource_ids] = value unless opts.key?(:resource_ids)
|
|
470
|
+
end
|
|
471
|
+
|
|
437
472
|
def validate_options!(opts)
|
|
438
473
|
if opts[:remote]
|
|
439
474
|
if opts[:resource]
|
|
@@ -26,6 +26,11 @@ module Graphiti
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
module Sugar
|
|
29
|
+
def self.included(base)
|
|
30
|
+
Graphiti::DEPRECATOR.warn("Graphiti::SpecHelpers::Sugar is deprecated. Call jsonapi_data, jsonapi_included, jsonapi_errors, json_date and json_datetime directly.")
|
|
31
|
+
super
|
|
32
|
+
end
|
|
33
|
+
|
|
29
34
|
def d
|
|
30
35
|
jsonapi_data
|
|
31
36
|
end
|
|
@@ -68,9 +68,9 @@ module Graphiti
|
|
|
68
68
|
# sideload can resolve it to something the foreign key alone would
|
|
69
69
|
# not predict, so the loaded records win. Only the un-included case
|
|
70
70
|
# is worth short-circuiting.
|
|
71
|
-
if sideload_ref.
|
|
71
|
+
if sideload_ref.resource_ids_from_foreign_key? &&
|
|
72
72
|
!self_ref.send(:included_anywhere?, @proxy.query.include_hash, sideload_ref.name)
|
|
73
|
-
linkage always: sideload_ref.
|
|
73
|
+
linkage always: sideload_ref.render_resource_ids? do
|
|
74
74
|
foreign_key = @object.public_send(sideload_ref.foreign_key)
|
|
75
75
|
|
|
76
76
|
unless foreign_key.nil?
|
|
@@ -81,7 +81,7 @@ module Graphiti
|
|
|
81
81
|
end
|
|
82
82
|
end
|
|
83
83
|
else
|
|
84
|
-
linkage always: sideload_ref.
|
|
84
|
+
linkage always: sideload_ref.render_resource_ids?
|
|
85
85
|
end
|
|
86
86
|
|
|
87
87
|
if link_ref
|
|
@@ -107,8 +107,31 @@ module Graphiti
|
|
|
107
107
|
|
|
108
108
|
def data_proc
|
|
109
109
|
sideload_ref = @sideload
|
|
110
|
+
resource_class_ref = @resource_class
|
|
110
111
|
->(_) {
|
|
111
|
-
|
|
112
|
+
begin
|
|
113
|
+
records = @object.public_send(sideload_ref.association_name)
|
|
114
|
+
rescue NoMethodError => error
|
|
115
|
+
# #receiver raises ArgumentError when the error was built by hand
|
|
116
|
+
# rather than raised by a failed call, and a hand-built one can
|
|
117
|
+
# still carry a matching #name.
|
|
118
|
+
receiver = begin
|
|
119
|
+
error.receiver
|
|
120
|
+
rescue ArgumentError
|
|
121
|
+
nil
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
raise unless error.name == sideload_ref.association_name &&
|
|
125
|
+
receiver.equal?(@object)
|
|
126
|
+
|
|
127
|
+
# A private method exists, so "has no such method" would be a lie.
|
|
128
|
+
raise if @object.respond_to?(sideload_ref.association_name, true)
|
|
129
|
+
|
|
130
|
+
raise Errors::MissingRelationshipMethod
|
|
131
|
+
.new(resource_class_ref, sideload_ref, @object)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
if records
|
|
112
135
|
if records.respond_to?(:to_ary)
|
|
113
136
|
records.each { |r| sideload_ref.resource.decorate_record(r) }
|
|
114
137
|
else
|
data/lib/graphiti/version.rb
CHANGED
data/lib/graphiti.rb
CHANGED
|
@@ -166,6 +166,8 @@ require "graphiti/configuration"
|
|
|
166
166
|
require "graphiti/context"
|
|
167
167
|
require "graphiti/errors"
|
|
168
168
|
require "graphiti/types"
|
|
169
|
+
require "graphiti/audit"
|
|
170
|
+
require "graphiti/audit/report"
|
|
169
171
|
require "graphiti/schema"
|
|
170
172
|
require "graphiti/schema_diff"
|
|
171
173
|
require "graphiti/adapters/abstract"
|
data/lib/tasks/graphiti.rake
CHANGED
|
@@ -16,6 +16,15 @@ namespace :graphiti do
|
|
|
16
16
|
Graphiti::Debugger.flush if debug
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
desc "Audit every relationship: what will raise, what loads to render ids, which render no ids, and which checks passed."
|
|
20
|
+
task audit: [:environment] do
|
|
21
|
+
helpers.setup_rails!
|
|
22
|
+
rows = Graphiti::Audit.run
|
|
23
|
+
puts Graphiti::Audit::Report.new(rows).to_s
|
|
24
|
+
|
|
25
|
+
exit 1 if rows.any?(&:error?)
|
|
26
|
+
end
|
|
27
|
+
|
|
19
28
|
desc "Execute benchmark without web server."
|
|
20
29
|
task :benchmark, [:path, :requests] => [:environment] do |_, args|
|
|
21
30
|
helpers.setup_rails!
|
data/package.json
CHANGED
data/website/sidebars.js
CHANGED
|
@@ -21,18 +21,18 @@ module.exports = {
|
|
|
21
21
|
type: 'category',
|
|
22
22
|
label: 'Topics',
|
|
23
23
|
items: [
|
|
24
|
-
'topics/authorization',
|
|
25
|
-
'topics/error-handling',
|
|
26
24
|
'topics/testing',
|
|
25
|
+
'topics/error-handling',
|
|
26
|
+
'topics/authorization',
|
|
27
27
|
'topics/debugging',
|
|
28
28
|
'topics/caching',
|
|
29
29
|
'topics/etags',
|
|
30
30
|
'topics/json-attributes',
|
|
31
|
-
'topics/
|
|
31
|
+
'topics/customizing-sideloads',
|
|
32
32
|
'concepts/backends-and-models',
|
|
33
33
|
'topics/without-activerecord',
|
|
34
34
|
'topics/openstruct-models',
|
|
35
|
-
'topics/
|
|
35
|
+
'topics/remote-resources',
|
|
36
36
|
'topics/hopping-relationships',
|
|
37
37
|
],
|
|
38
38
|
},
|
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.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lee Richmond
|
|
@@ -335,6 +335,8 @@ files:
|
|
|
335
335
|
- lib/graphiti/adapters/graphiti_api.rb
|
|
336
336
|
- lib/graphiti/adapters/null.rb
|
|
337
337
|
- lib/graphiti/adapters/persistence/associations.rb
|
|
338
|
+
- lib/graphiti/audit.rb
|
|
339
|
+
- lib/graphiti/audit/report.rb
|
|
338
340
|
- lib/graphiti/cli.rb
|
|
339
341
|
- lib/graphiti/configuration.rb
|
|
340
342
|
- lib/graphiti/context.rb
|