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.
@@ -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
@@ -3,8 +3,13 @@ module Graphiti
3
3
  attr_reader :params
4
4
  attr_reader :deserialized_payload
5
5
 
6
- # TODO: make query and action keywords once the satellite gems are rolled in - they instantiate Runner positionally
7
- def initialize(resource_class, params, query = nil, action = nil)
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, nil, nil, [], @action)
38
+ @query ||= Query.new(jsonapi_resource, params, action: @action)
34
39
  end
35
40
 
36
41
  def query_hash
@@ -251,7 +251,7 @@ module Graphiti
251
251
  schema[:guard] = true
252
252
  end
253
253
 
254
- if config.always_include_resource_ids?
254
+ if config.render_resource_ids?
255
255
  schema[:linkage] = true
256
256
  end
257
257
 
@@ -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 default_include_resource_ids?
7
- linkage_from_foreign_key?
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
- # The parent already carries the foreign key, and for a plain belongs_to
11
- # that key *is* the related id, so linkage costs nothing. Anything that can
12
- # change which record the relationship resolves to, or what type it carries,
13
- # has to load the association instead:
14
- #
15
- # - a scope/params block or a base_scope can filter out the record the
16
- # foreign key points at, so the key would claim a relationship the API
17
- # would not actually return
18
- # - a polymorphic target takes its type from the record, not from the
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
- true
28
+ nil
35
29
  end
36
30
 
37
- # The foreign key can stand in for the related id only when the two hold the
38
- # same value. base_filter matches the key against primary_key, so pointing
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
@@ -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
- @always_include_resource_ids = opts[:always_include_resource_ids]
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
- # False everywhere but a plain belongs_to - see
126
- # Sideload::BelongsTo#linkage_from_foreign_key?.
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
- # nil at either of the first two levels means "not specified" rather
132
- # than "false".
133
- def always_include_resource_ids?
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
- configured = parent_resource_class&.always_include_resource_ids_by_default
137
- return !!configured unless configured.nil?
159
+ def render_resource_ids?
160
+ return !!@render_resource_ids unless @render_resource_ids.nil?
138
161
 
139
- default_include_resource_ids?
162
+ default_render_resource_ids?
140
163
  end
141
164
 
142
- def default_include_resource_ids?
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.linkage_from_foreign_key? &&
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.always_include_resource_ids? do
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.always_include_resource_ids?
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
- if (records = @object.public_send(sideload_ref.association_name))
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
@@ -1,3 +1,3 @@
1
1
  module Graphiti
2
- VERSION = "2.0.0.beta.5"
2
+ VERSION = "2.0.0.beta.7"
3
3
  end
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"
@@ -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
@@ -22,6 +22,7 @@
22
22
  },
23
23
  "release": {
24
24
  "branches": [
25
+ "1.x",
25
26
  "main",
26
27
  {
27
28
  "name": "beta",
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/remote-resources',
31
+ 'topics/customizing-sideloads',
32
32
  'concepts/backends-and-models',
33
33
  'topics/without-activerecord',
34
34
  'topics/openstruct-models',
35
- 'topics/customizing-sideloads',
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.5
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