graphiti 2.0.0.beta.9 → 2.0.0.beta.11

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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +2 -0
  3. data/.tool-versions +1 -1
  4. data/CHANGELOG.md +46 -0
  5. data/Rakefile +17 -12
  6. data/lib/generators/graphiti/generator_mixin.rb +24 -0
  7. data/lib/generators/graphiti/templates/application_resource.rb.erb +9 -16
  8. data/lib/graphiti/adapters/abstract.rb +4 -3
  9. data/lib/graphiti/configuration.rb +55 -24
  10. data/lib/graphiti/delegates/pagination.rb +2 -2
  11. data/lib/graphiti/errors.rb +35 -3
  12. data/lib/graphiti/query.rb +61 -13
  13. data/lib/graphiti/rails/controller.rb +26 -6
  14. data/lib/graphiti/rails/rake_helpers.rb +17 -0
  15. data/lib/graphiti/renderer.rb +4 -1
  16. data/lib/graphiti/request_validators/update_validator.rb +1 -1
  17. data/lib/graphiti/request_validators/validator.rb +9 -0
  18. data/lib/graphiti/resource/configuration.rb +139 -40
  19. data/lib/graphiti/resource/dsl.rb +21 -2
  20. data/lib/graphiti/resource/interface.rb +1 -1
  21. data/lib/graphiti/resource/links.rb +39 -5
  22. data/lib/graphiti/resource/remote.rb +2 -1
  23. data/lib/graphiti/resource/sideloading.rb +2 -2
  24. data/lib/graphiti/schema.rb +2 -2
  25. data/lib/graphiti/scope.rb +100 -26
  26. data/lib/graphiti/scoping/filter.rb +4 -4
  27. data/lib/graphiti/scoping/paginate.rb +3 -3
  28. data/lib/graphiti/serializer.rb +30 -8
  29. data/lib/graphiti/sideload/polymorphic_belongs_to.rb +9 -9
  30. data/lib/graphiti/sideload.rb +50 -33
  31. data/lib/graphiti/spec_helpers/matchers.rb +2 -2
  32. data/lib/graphiti/spec_helpers/rspec.rb +6 -3
  33. data/lib/graphiti/util/link.rb +1 -1
  34. data/lib/graphiti/util/serializer_attributes.rb +5 -3
  35. data/lib/graphiti/util/serializer_relationships.rb +16 -15
  36. data/lib/graphiti/version.rb +1 -1
  37. data/lib/graphiti.rb +5 -0
  38. data/lib/tasks/graphiti.rake +4 -0
  39. data/package.json +1 -1
  40. metadata +1 -1
@@ -24,6 +24,16 @@ module Graphiti
24
24
  self.relationship_condition_blocks ||= {}
25
25
  self.relationship_sideloads ||= {}
26
26
 
27
+ # Keyed on the sideloads hash itself, which is reassigned whenever a
28
+ # relationship is applied, so the answer is recomputed exactly then.
29
+ def self.on_demand_links?
30
+ sideloads = relationship_sideloads
31
+ return @on_demand_links.last if @on_demand_links&.first.equal?(sideloads)
32
+
33
+ @on_demand_links = [sideloads, sideloads.each_value.any? { |sideload| sideload.link_mode == :on_demand }]
34
+ @on_demand_links.last
35
+ end
36
+
27
37
  def self.inherited(klass)
28
38
  super
29
39
  klass.class_eval do
@@ -59,7 +69,7 @@ module Graphiti
59
69
  starting_offset = 0
60
70
  page_param = @proxy.query.pagination
61
71
  if (page_number = page_param[:number])
62
- page_size = page_param[:size] || @resource.default_page_size
72
+ page_size = page_param[:size] || @resource.page_default_size
63
73
  starting_offset = (page_number - 1) * page_size
64
74
  end
65
75
 
@@ -74,7 +84,7 @@ module Graphiti
74
84
 
75
85
  def as_jsonapi(kwargs = {})
76
86
  super(**kwargs).tap do |hash|
77
- strip_relationships!(hash) if strip_relationships?
87
+ strip_relationships!(hash)
78
88
  add_links!(hash)
79
89
  end
80
90
  end
@@ -100,17 +110,29 @@ module Graphiti
100
110
  hash[:links] = @resource.links(@object) if @resource.links?
101
111
  end
102
112
 
113
+ # The meta: {included: false} stub is jsonapi-serializable's filler, not JSON:API.
103
114
  def strip_relationships!(hash)
104
- hash[:relationships]&.select! do |name, payload|
105
- payload.key?(:data)
115
+ placeholders = relationship_placeholders?
116
+ return if placeholders && !strip_on_demand_relationships?
117
+
118
+ hash[:relationships]&.reject! do |name, payload|
119
+ next false if payload.key?(:data) || payload.key?(:links)
120
+ next true unless placeholders
121
+
122
+ self.class.relationship_sideloads[name]&.link_mode == :on_demand
106
123
  end
107
124
  end
108
125
 
109
- def strip_relationships?
110
- return false unless Graphiti.config.links_on_demand
111
- params = Graphiti.context[:object]&.params || {}
126
+ # A remote resource exposes a stand-in object rather than a Resource.
127
+ def relationship_placeholders?
128
+ resource_class = @resource.class
129
+ return Resource.relationship_placeholders unless resource_class.respond_to?(:relationship_placeholders)
130
+
131
+ resource_class.relationship_placeholders
132
+ end
112
133
 
113
- [false, nil, "false"].include?(params[:links])
134
+ def strip_on_demand_relationships?
135
+ self.class.on_demand_links? && !@proxy&.query&.render_link?(:on_demand)
114
136
  end
115
137
  end
116
138
  end
@@ -107,18 +107,18 @@ class Graphiti::Sideload::PolymorphicBelongsTo < Graphiti::Sideload::BelongsTo
107
107
  end
108
108
  end
109
109
 
110
- def resolve(parents, query, graph_parent)
110
+ def resolve(parents, query, graph_parent, &proxy_block)
111
111
  if ::Graphiti::Scope.resolve_synchronously?
112
- sync_resolve(parents, query, graph_parent)
112
+ sync_resolve(parents, query, graph_parent, &proxy_block)
113
113
  else
114
- future_resolve(parents, query, graph_parent).value!
114
+ future_resolve(parents, query, graph_parent, &proxy_block).value!
115
115
  end
116
116
  end
117
117
 
118
- def future_resolve(parents, query, graph_parent)
118
+ def future_resolve(parents, query, graph_parent, &proxy_block)
119
119
  promises = []
120
120
  each_resolvable_group(parents, query) do |child, group, child_query|
121
- promises << child.future_resolve(group, child_query, graph_parent)
121
+ promises << child.future_resolve(group, child_query, graph_parent, &proxy_block)
122
122
  end
123
123
  return promises.first if promises.one?
124
124
 
@@ -130,14 +130,14 @@ class Graphiti::Sideload::PolymorphicBelongsTo < Graphiti::Sideload::BelongsTo
130
130
  end
131
131
  end
132
132
 
133
- private
134
-
135
- def sync_resolve(parents, query, graph_parent)
133
+ def sync_resolve(parents, query, graph_parent, &proxy_block)
136
134
  each_resolvable_group(parents, query) do |child, group, child_query|
137
- child.resolve(group, child_query, graph_parent)
135
+ child.sync_resolve(group, child_query, graph_parent, &proxy_block)
138
136
  end
139
137
  end
140
138
 
139
+ private
140
+
141
141
  # Group parents by their polymorphic type and yield each group's child
142
142
  # sideload alongside a query pruned to the sideloads that child supports.
143
143
  def each_resolvable_group(parents, query)
@@ -31,6 +31,9 @@ module Graphiti
31
31
  @writable = opts[:writable]
32
32
  @as = opts[:as]
33
33
  @link = opts[:link]
34
+ unless @link.nil? || Resource::LINK_MODES.include?(@link)
35
+ raise Errors::InvalidLinkRendering.new(@parent_resource_class, :"#{name} link", @link)
36
+ end
34
37
  @single = opts[:single]
35
38
  @remote = opts[:remote]
36
39
  apply_belongs_to_many_filter if type == :many_to_many
@@ -84,7 +87,8 @@ module Graphiti
84
87
  self.adapter = Graphiti::Adapters::GraphitiAPI
85
88
  self.model = OpenStruct
86
89
  self.remote = remote_url
87
- self.validate_endpoints = false
90
+ self.validate_requests = false
91
+ self.validate_links = false
88
92
  }
89
93
  name = "#{parent_resource_class.name}.#{@name}.remote"
90
94
  klass.class_eval("def self.name;'#{name}';end", __FILE__, __LINE__)
@@ -148,8 +152,12 @@ module Graphiti
148
152
  !!@polymorphic_as
149
153
  end
150
154
 
155
+ # Every check behind the blocker is static sideload configuration, and this
156
+ # is asked once per rendered record per relationship.
151
157
  def resource_ids_from_foreign_key?
152
- resource_ids_blocker.nil?
158
+ return @resource_ids_from_foreign_key unless @resource_ids_from_foreign_key.nil?
159
+
160
+ @resource_ids_from_foreign_key = resource_ids_blocker.nil?
153
161
  end
154
162
 
155
163
  def resource_ids_blocker
@@ -166,14 +174,16 @@ module Graphiti
166
174
  false
167
175
  end
168
176
 
169
- def link?
170
- return true if link_proc
177
+ # A custom link block means the author wants the link, so a false default does not silence it.
178
+ def link_mode
179
+ return @link unless @link.nil?
171
180
 
172
- if @link.nil?
173
- !!@parent_resource_class.autolink
174
- else
175
- !!@link
176
- end
181
+ default = @parent_resource_class.relationship_links
182
+ (link_proc && default == false) ? true : default
183
+ end
184
+
185
+ def link?
186
+ link_mode != false
177
187
  end
178
188
 
179
189
  def link_filter(parents)
@@ -279,14 +289,20 @@ module Graphiti
279
289
  proxy
280
290
  end
281
291
 
282
- def load(parents, query, graph_parent)
292
+ def load(parents, query, graph_parent, &proxy_block)
283
293
  if Scope.resolve_synchronously?
284
- build_resource_proxy(parents, query, graph_parent).to_a
294
+ sync_load(parents, query, graph_parent, &proxy_block)
285
295
  else
286
- future_load(parents, query, graph_parent).value!
296
+ future_load(parents, query, graph_parent, &proxy_block).value!
287
297
  end
288
298
  end
289
299
 
300
+ def sync_load(parents, query, graph_parent, &proxy_block)
301
+ proxy = build_resource_proxy(parents, query, graph_parent)
302
+ proxy_block&.call(proxy)
303
+ proxy.to_a
304
+ end
305
+
290
306
  # Override in subclass
291
307
  def infer_foreign_key
292
308
  model = parent_resource_class.model
@@ -334,15 +350,30 @@ module Graphiti
334
350
  children.replace(associated) if track_associated
335
351
  end
336
352
 
337
- def resolve(parents, query, graph_parent)
353
+ def resolve(parents, query, graph_parent, &proxy_block)
338
354
  if Scope.resolve_synchronously?
339
- sync_resolve(parents, query, graph_parent)
355
+ sync_resolve(parents, query, graph_parent, &proxy_block)
340
356
  else
341
- future_resolve(parents, query, graph_parent).value!
357
+ future_resolve(parents, query, graph_parent, &proxy_block).value!
342
358
  end
343
359
  end
344
360
 
345
- def future_resolve(parents, query, graph_parent)
361
+ # Called by a scope that already decided to stay inline, so it must not consult the pool again.
362
+ # A scope_proc builds a Scope rather than a proxy, and that nested scope decides for itself.
363
+ def sync_resolve(parents, query, graph_parent, &proxy_block)
364
+ assert_singular!(parents)
365
+
366
+ if self.class.scope_proc
367
+ build_sideload_scope(parents, query, graph_parent).resolve do |sideload_results|
368
+ fire_assign(parents, sideload_results)
369
+ end
370
+ else
371
+ sync_load(parents, query, graph_parent, &proxy_block)
372
+ end
373
+ end
374
+
375
+ # A scope_proc builds a Scope rather than a proxy, and a Scope's cache key omits what a proxy's carries.
376
+ def future_resolve(parents, query, graph_parent, &proxy_block)
346
377
  assert_singular!(parents)
347
378
 
348
379
  if self.class.scope_proc
@@ -350,7 +381,7 @@ module Graphiti
350
381
  fire_assign(parents, sideload_results)
351
382
  end
352
383
  else
353
- future_load(parents, query, graph_parent)
384
+ future_load(parents, query, graph_parent, &proxy_block)
354
385
  end
355
386
  end
356
387
 
@@ -414,21 +445,6 @@ module Graphiti
414
445
 
415
446
  private
416
447
 
417
- # Synchronous counterpart to #future_resolve, used when concurrency is off.
418
- # Resolves inline via the synchronous Scope#resolve / #load paths (no
419
- # promises). See Scope#sync_resolve_sideloads.
420
- def sync_resolve(parents, query, graph_parent)
421
- assert_singular!(parents)
422
-
423
- if self.class.scope_proc
424
- build_sideload_scope(parents, query, graph_parent).resolve do |sideload_results|
425
- fire_assign(parents, sideload_results)
426
- end
427
- else
428
- load(parents, query, graph_parent)
429
- end
430
- end
431
-
432
448
  def assert_singular!(parents)
433
449
  if single? && parents.length > 1
434
450
  raise Errors::SingularSideload.new(self, parents.length)
@@ -445,8 +461,9 @@ module Graphiti
445
461
  default_paginate: false
446
462
  end
447
463
 
448
- def future_load(parents, query, graph_parent)
464
+ def future_load(parents, query, graph_parent, &proxy_block)
449
465
  proxy = build_resource_proxy(parents, query, graph_parent)
466
+ proxy_block&.call(proxy)
450
467
  proxy.respond_to?(:future_resolve_data) ? proxy.future_resolve_data : Concurrent::Promises.fulfilled_future(proxy)
451
468
  end
452
469
 
@@ -140,7 +140,7 @@ module Graphiti
140
140
  end
141
141
 
142
142
  class FilterAttributeMatcher < ResourceDSLMatcher
143
- GRAPHITI_OPTS = %i[allow deny single required allow_nil deny_empty].freeze
143
+ GRAPHITI_OPTS = %i[allow deny single required blanks].freeze
144
144
  GRAPHITI_CONFIG_KEY = :filters
145
145
  EXPECTED_ACTION = "filter"
146
146
  end
@@ -186,7 +186,7 @@ module Graphiti
186
186
  # @param [Symbol] type
187
187
  #
188
188
  # @example expect(subject).to filter_attribute(:name, :string)
189
- # @example expect(subject).to filter_attribute(:name, :string).with_options(allow_nil: false)
189
+ # @example expect(subject).to filter_attribute(:name, :string).with_options(blanks: :null)
190
190
  # @example expect(subject).not_to filter_attribute(:name, :string)
191
191
  def filter_attribute(attribute, type)
192
192
  FilterAttributeMatcher.new(attribute, type)
@@ -39,14 +39,17 @@ resource_testing = proc do
39
39
  let(:params) { {} }
40
40
 
41
41
  around do |e|
42
- original = Graphiti::Resource.validate_endpoints
43
- Graphiti::Resource.validate_endpoints = false
42
+ original_requests = Graphiti::Resource.validate_requests
43
+ original_links = Graphiti::Resource.validate_links
44
+ Graphiti::Resource.validate_requests = false
45
+ Graphiti::Resource.validate_links = false
44
46
 
45
47
  Graphiti.with_context graphiti_context do
46
48
  e.run
47
49
  end
48
50
  ensure
49
- Graphiti::Resource.validate_endpoints = original
51
+ Graphiti::Resource.validate_requests = original_requests
52
+ Graphiti::Resource.validate_links = original_links
50
53
  end
51
54
 
52
55
  def graphiti_context
@@ -47,7 +47,7 @@ module Graphiti
47
47
  end
48
48
 
49
49
  def on_demand_links(url)
50
- return url unless Graphiti.config.links_on_demand
50
+ return url unless @sideload.resource.relationship_links == :on_demand
51
51
  return unless url
52
52
 
53
53
  url << if url.include?("?")
@@ -30,7 +30,7 @@ module Graphiti
30
30
  @serializer.send(:"#{applied_method}=", [@name] | existing)
31
31
 
32
32
  @serializer.meta do
33
- if !!@resource.try(:cursor_paginatable?) && !Graphiti.context[:graphql]
33
+ if !!@resource.try(:page_cursors?) && !Graphiti.context[:graphql]
34
34
  {cursor: cursor}
35
35
  end
36
36
  end
@@ -121,10 +121,11 @@ module Graphiti
121
121
 
122
122
  def default_proc
123
123
  name_ref = @name
124
+ resource_ref = @resource
124
125
  typecast_ref = typecast(Graphiti::Types[@attr[:type]][:read])
125
126
  ->(_) {
126
127
  val = @object.send(name_ref)
127
- if Graphiti.config.typecast_reads
128
+ if resource_ref.typecast_reads
128
129
  typecast_ref.call(val)
129
130
  else
130
131
  val
@@ -133,10 +134,11 @@ module Graphiti
133
134
  end
134
135
 
135
136
  def wrap_proc(inner)
137
+ resource_ref = @resource
136
138
  typecast_ref = typecast(Graphiti::Types[@attr[:type]][:read])
137
139
  ->(serializer_instance = nil) {
138
140
  val = serializer_instance.instance_eval(&inner)
139
- if Graphiti.config.typecast_reads
141
+ if resource_ref.typecast_reads
140
142
  typecast_ref.call(val)
141
143
  else
142
144
  val
@@ -8,6 +8,8 @@ module Graphiti
8
8
  end
9
9
 
10
10
  def apply
11
+ return unless @serializer
12
+
11
13
  @sideloads.each_pair do |name, sideload|
12
14
  if apply?(sideload)
13
15
  SerializerRelationship
@@ -55,7 +57,6 @@ module Graphiti
55
57
  private
56
58
 
57
59
  def block
58
- link_ref = link?
59
60
  sideload_ref = @sideload
60
61
  resource_class_ref = @resource_class
61
62
  data_proc_ref = data_proc
@@ -93,13 +94,11 @@ module Graphiti
93
94
  linkage always: sideload_ref.render_resource_ids?
94
95
  end
95
96
 
96
- if link_ref
97
- if @proxy.query.links?
98
- self_ref.send(:validate_link!) unless self_ref.send(:eagerly_validate_links?)
97
+ if @proxy.query.render_link?(sideload_ref.link_mode) && self_ref.send(:linkable?)
98
+ self_ref.send(:validate_link!) unless self_ref.send(:eagerly_validate_links?)
99
99
 
100
- link(:related) do
101
- ::Graphiti::Util::Link.new(sideload_ref, @object).generate
102
- end
100
+ link(:related) do
101
+ ::Graphiti::Util::Link.new(sideload_ref, @object).generate
103
102
  end
104
103
  end
105
104
  end
@@ -162,8 +161,8 @@ module Graphiti
162
161
  end
163
162
 
164
163
  def validate_link!
165
- return unless link?
166
- return unless @resource_class.validate_endpoints?
164
+ return unless @sideload.link? && linkable?
165
+ return unless @resource_class.validate_links?
167
166
  return if @sideload.link_proc
168
167
 
169
168
  unless Graphiti.config.context_for_endpoint
@@ -192,14 +191,16 @@ module Graphiti
192
191
  self.class.validated_link_cache << cache_key
193
192
  end
194
193
 
195
- def link?
196
- return true if @sideload.link_proc
194
+ # Checked lazily so a sideload with no endpoint only raises when a link is actually wanted.
195
+ def linkable?
196
+ return @linkable if defined?(@linkable)
197
197
 
198
- if @sideload.respond_to?(:children)
199
- @sideload.link? &&
200
- @sideload.children.values.all? { |c| !c.resource.endpoint.nil? }
198
+ @linkable = if @sideload.link_proc
199
+ true
200
+ elsif @sideload.respond_to?(:children)
201
+ @sideload.children.values.all? { |c| !c.resource.endpoint.nil? }
201
202
  else
202
- !!(@sideload.link? && @sideload.resource.endpoint)
203
+ !@sideload.resource.endpoint.nil?
203
204
  end
204
205
  end
205
206
  end
@@ -1,3 +1,3 @@
1
1
  module Graphiti
2
- VERSION = "2.0.0.beta.9"
2
+ VERSION = "2.0.0.beta.11"
3
3
  end
data/lib/graphiti.rb CHANGED
@@ -149,6 +149,11 @@ module Graphiti
149
149
  resources.each do |r|
150
150
  r.apply_sideloads_to_serializer
151
151
  end
152
+ @setup = true
153
+ end
154
+
155
+ def self.setup?
156
+ !!@setup
152
157
  end
153
158
 
154
159
  def self.cache=(val)
@@ -22,6 +22,10 @@ namespace :graphiti do
22
22
  rows = Graphiti::Audit.run
23
23
  puts Graphiti::Audit::Report.new(rows)
24
24
 
25
+ if (advisory = helpers.connection_pool_advisory)
26
+ puts advisory
27
+ end
28
+
25
29
  exit 1 if rows.any?(&:error?)
26
30
  end
27
31
 
data/package.json CHANGED
@@ -142,7 +142,7 @@
142
142
  [
143
143
  "@semantic-release/exec",
144
144
  {
145
- "prepareCmd": "bundle exec ruby spec/performance/measure_releases.rb --pending v${nextRelease.version}"
145
+ "prepareCmd": "bundle exec ruby spec/performance/measure_releases.rb --promote v${nextRelease.version}"
146
146
  }
147
147
  ],
148
148
  [
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.9
4
+ version: 2.0.0.beta.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lee Richmond