graphiti 1.2.31 → 1.3.4

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/CHANGELOG.md +3 -0
  3. data/deprecated_generators/graphiti/generator_mixin.rb +1 -0
  4. data/lib/graphiti/adapters/abstract.rb +3 -2
  5. data/lib/graphiti/adapters/active_record.rb +7 -3
  6. data/lib/graphiti/adapters/graphiti_api.rb +1 -1
  7. data/lib/graphiti/adapters/null.rb +1 -1
  8. data/lib/graphiti/adapters/persistence/associations.rb +10 -2
  9. data/lib/graphiti/configuration.rb +3 -1
  10. data/lib/graphiti/delegates/pagination.rb +33 -10
  11. data/lib/graphiti/errors.rb +71 -11
  12. data/lib/graphiti/extensions/extra_attribute.rb +3 -3
  13. data/lib/graphiti/hash_renderer.rb +194 -21
  14. data/lib/graphiti/query.rb +32 -6
  15. data/lib/graphiti/renderer.rb +19 -1
  16. data/lib/graphiti/request_validators/update_validator.rb +3 -3
  17. data/lib/graphiti/request_validators/validator.rb +29 -21
  18. data/lib/graphiti/resource/configuration.rb +22 -1
  19. data/lib/graphiti/resource/dsl.rb +20 -2
  20. data/lib/graphiti/resource/interface.rb +1 -1
  21. data/lib/graphiti/resource/polymorphism.rb +6 -1
  22. data/lib/graphiti/resource/remote.rb +1 -1
  23. data/lib/graphiti/resource.rb +2 -1
  24. data/lib/graphiti/resource_proxy.rb +12 -0
  25. data/lib/graphiti/schema.rb +27 -4
  26. data/lib/graphiti/schema_diff.rb +44 -4
  27. data/lib/graphiti/scope.rb +2 -2
  28. data/lib/graphiti/scoping/filter.rb +19 -8
  29. data/lib/graphiti/scoping/filter_group_validator.rb +78 -0
  30. data/lib/graphiti/scoping/paginate.rb +47 -3
  31. data/lib/graphiti/serializer.rb +41 -6
  32. data/lib/graphiti/sideload.rb +16 -1
  33. data/lib/graphiti/util/class.rb +6 -0
  34. data/lib/graphiti/util/link.rb +4 -0
  35. data/lib/graphiti/util/remote_params.rb +9 -4
  36. data/lib/graphiti/util/remote_serializer.rb +1 -0
  37. data/lib/graphiti/util/serializer_attributes.rb +37 -10
  38. data/lib/graphiti/version.rb +1 -1
  39. data/lib/graphiti.rb +2 -0
  40. metadata +4 -3
@@ -0,0 +1,78 @@
1
+ module Graphiti
2
+ class Scoping::FilterGroupValidator
3
+ VALID_REQUIRED_VALUES = %i[all any]
4
+
5
+ def self.raise_unless_filter_group_requirement_valid!(resource, requirement)
6
+ unless VALID_REQUIRED_VALUES.include?(requirement)
7
+ raise Errors::FilterGroupInvalidRequirement.new(
8
+ resource,
9
+ VALID_REQUIRED_VALUES
10
+ )
11
+ end
12
+
13
+ true
14
+ end
15
+
16
+ def initialize(resource, query_hash)
17
+ @resource = resource
18
+ @query_hash = query_hash
19
+ end
20
+
21
+ def raise_unless_filter_group_requirements_met!
22
+ return if grouped_filters.empty?
23
+
24
+ case filter_group_requirement
25
+ when :all
26
+ raise_unless_all_requirements_met!
27
+ when :any
28
+ raise_unless_any_requirements_met!
29
+ end
30
+
31
+ true
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :resource, :query_hash
37
+
38
+ def raise_unless_all_requirements_met!
39
+ met = filter_group_names.all? { |filter_name| filter_group_filter_param.key?(filter_name) }
40
+
41
+ unless met
42
+ raise Errors::FilterGroupMissingRequiredFilters.new(
43
+ resource,
44
+ filter_group_names,
45
+ filter_group_requirement
46
+ )
47
+ end
48
+ end
49
+
50
+ def raise_unless_any_requirements_met!
51
+ met = filter_group_names.any? { |filter_name| filter_group_filter_param.key?(filter_name) }
52
+
53
+ unless met
54
+ raise Errors::FilterGroupMissingRequiredFilters.new(
55
+ resource,
56
+ filter_group_names,
57
+ filter_group_requirement
58
+ )
59
+ end
60
+ end
61
+
62
+ def filter_group_names
63
+ grouped_filters.fetch(:names, [])
64
+ end
65
+
66
+ def filter_group_requirement
67
+ grouped_filters.fetch(:required, :invalid)
68
+ end
69
+
70
+ def grouped_filters
71
+ resource.grouped_filters
72
+ end
73
+
74
+ def filter_group_filter_param
75
+ query_hash.fetch(:filter, {})
76
+ end
77
+ end
78
+ end
@@ -1,6 +1,7 @@
1
1
  module Graphiti
2
2
  class Scoping::Paginate < Scoping::Base
3
3
  DEFAULT_PAGE_SIZE = 20
4
+ PARAMS = [:number, :size, :offset, :before, :after]
4
5
 
5
6
  def apply
6
7
  if size > resource.max_page_size
@@ -33,24 +34,67 @@ module Graphiti
33
34
 
34
35
  # Apply default pagination proc via the Resource adapter
35
36
  def apply_standard_scope
36
- resource.adapter.paginate(@scope, number, size)
37
+ meth = resource.adapter.method(:paginate)
38
+
39
+ if meth.arity == 4 # backwards-compat
40
+ resource.adapter.paginate(@scope, number, size, offset)
41
+ else
42
+ resource.adapter.paginate(@scope, number, size)
43
+ end
37
44
  end
38
45
 
39
46
  # Apply the custom pagination proc
40
47
  def apply_custom_scope
41
- resource.instance_exec(@scope, number, size, resource.context, &custom_scope)
48
+ resource.instance_exec \
49
+ @scope,
50
+ number,
51
+ size,
52
+ resource.context,
53
+ offset,
54
+ &custom_scope
42
55
  end
43
56
 
44
57
  private
45
58
 
46
59
  def requested?
47
- ![page_param[:size], page_param[:number]].all?(&:nil?)
60
+ !PARAMS.map { |p| page_param[p] }.all?(&:nil?)
48
61
  end
49
62
 
50
63
  def page_param
51
64
  @page_param ||= (query_hash[:page] || {})
52
65
  end
53
66
 
67
+ def offset
68
+ offset = nil
69
+
70
+ if (value = page_param[:offset])
71
+ offset = value.to_i
72
+ end
73
+
74
+ if before_cursor&.key?(:offset)
75
+ if page_param.key?(:number)
76
+ raise Errors::UnsupportedBeforeCursor
77
+ end
78
+
79
+ offset = before_cursor[:offset] - (size * number) - 1
80
+ offset = 0 if offset.negative?
81
+ end
82
+
83
+ if after_cursor&.key?(:offset)
84
+ offset = after_cursor[:offset]
85
+ end
86
+
87
+ offset
88
+ end
89
+
90
+ def after_cursor
91
+ page_param[:after]
92
+ end
93
+
94
+ def before_cursor
95
+ page_param[:before]
96
+ end
97
+
54
98
  def number
55
99
  (page_param[:number] || 1).to_i
56
100
  end
@@ -12,14 +12,55 @@ module Graphiti
12
12
  # go through type checking/coercion
13
13
  class_attribute :attributes_applied_via_resource
14
14
  class_attribute :extra_attributes_applied_via_resource
15
+ class_attribute :relationship_condition_blocks
15
16
  self.attributes_applied_via_resource = []
16
17
  self.extra_attributes_applied_via_resource = []
18
+ # See #requested_relationships
19
+ self.relationship_condition_blocks ||= {}
17
20
 
18
21
  def self.inherited(klass)
19
22
  super
20
23
  klass.class_eval do
21
24
  extend JSONAPI::Serializable::Resource::ConditionalFields
25
+
26
+ # See #requested_relationships
27
+ def self.relationship(name, options = {}, &block)
28
+ prev = Util::Hash.deep_dup(field_condition_blocks)
29
+ super
30
+ self.field_condition_blocks = prev
31
+ _register_condition(relationship_condition_blocks, name, options)
32
+ end
33
+
34
+ # NB - avoid clobbering includes when sparse fieldset
35
+ # https://github.com/jsonapi-rb/jsonapi-serializable/pull/102
36
+ #
37
+ # We also override this method to ensure attributes and relationships
38
+ # have separate condition blocks. This way an attribute and
39
+ # relationship can have the same name, and the attribute can be
40
+ # conditional without affecting the relationship.
41
+ def requested_relationships(fields)
42
+ @_relationships.select do |k, _|
43
+ _conditionally_included?(self.class.relationship_condition_blocks, k)
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ def cursor
50
+ starting_offset = 0
51
+ page_param = @proxy.query.pagination
52
+ if (page_number = page_param[:number])
53
+ page_size = page_param[:size] || @resource.default_page_size
54
+ starting_offset = (page_number - 1) * page_size
22
55
  end
56
+
57
+ if (cursor = page_param[:after])
58
+ starting_offset = cursor[:offset]
59
+ end
60
+
61
+ current_offset = @object.instance_variable_get(:@__graphiti_index)
62
+ offset = starting_offset + current_offset + 1 # (+ 1 b/c o-base index)
63
+ Base64.encode64({offset: offset}.to_json).chomp
23
64
  end
24
65
 
25
66
  def as_jsonapi(kwargs = {})
@@ -29,12 +70,6 @@ module Graphiti
29
70
  end
30
71
  end
31
72
 
32
- # Temporary fix until fixed upstream
33
- # https://github.com/jsonapi-rb/jsonapi-serializable/pull/102
34
- def requested_relationships(fields)
35
- @_relationships
36
- end
37
-
38
73
  # Allow access to resource methods
39
74
  def method_missing(id, *args, &blk)
40
75
  if @resource.respond_to?(id, true)
@@ -136,6 +136,19 @@ module Graphiti
136
136
  base_filter(parents)
137
137
  end
138
138
 
139
+ def link_extra_fields
140
+ return unless context&.respond_to?(:params)
141
+
142
+ extra_fields_name = [association_name, resource.type].find { |param|
143
+ context.params.dig(:extra_fields, param)
144
+ }
145
+
146
+ if extra_fields_name
147
+ extra_fields = context.params.dig(:extra_fields, extra_fields_name)
148
+ {resource.type => extra_fields}
149
+ end
150
+ end
151
+
139
152
  # The parent resource is a remote,
140
153
  # AND the sideload is a remote to the same endpoint
141
154
  def shared_remote?
@@ -209,7 +222,9 @@ module Graphiti
209
222
  end
210
223
 
211
224
  with_error_handling(Errors::SideloadQueryBuildingError) do
212
- proxy = resource.class._all(params, opts, base_scope)
225
+ scope = base_scope
226
+ scope[:foreign_key] = foreign_key if remote?
227
+ proxy = resource.class._all(params, opts, scope)
213
228
  pre_load_proc&.call(proxy, parents)
214
229
  end
215
230
 
@@ -20,6 +20,12 @@ module Graphiti
20
20
  split = namespace.split("::")
21
21
  split[0, split.length - 1].join("::")
22
22
  end
23
+
24
+ def self.graphql_type_name(name)
25
+ name.gsub("Resource", "")
26
+ .gsub("::", "") # remove modules
27
+ .gsub(".", "__") # remove remote resource .
28
+ end
23
29
  end
24
30
  end
25
31
  end
@@ -63,6 +63,10 @@ module Graphiti
63
63
  params[:filter] = @sideload.link_filter([@model])
64
64
  end
65
65
 
66
+ if (extra_fields = @sideload.link_extra_fields)
67
+ params[:extra_fields] ||= extra_fields
68
+ end
69
+
66
70
  @sideload.params_proc&.call(params, [@model], context)
67
71
  end
68
72
  end
@@ -2,13 +2,14 @@
2
2
  module Graphiti
3
3
  module Util
4
4
  class RemoteParams
5
- def self.generate(resource, query)
6
- new(resource, query).generate
5
+ def self.generate(resource, query, foreign_key)
6
+ new(resource, query, foreign_key).generate
7
7
  end
8
8
 
9
- def initialize(resource, query)
9
+ def initialize(resource, query, foreign_key)
10
10
  @resource = resource
11
11
  @query = query
12
+ @foreign_key = foreign_key
12
13
  @sorts = []
13
14
  @filters = {}
14
15
  @fields = {}
@@ -97,7 +98,11 @@ module Graphiti
97
98
  return unless fields
98
99
 
99
100
  fields.each_pair do |type, attrs|
100
- @fields[type] = attrs.join(",")
101
+ all_attrs = attrs
102
+ if @foreign_key
103
+ all_attrs |= [@foreign_key]
104
+ end
105
+ @fields[type] = all_attrs.join(",")
101
106
  end
102
107
  end
103
108
 
@@ -44,6 +44,7 @@ module Graphiti
44
44
  model.delete_field(:_relationships)
45
45
  # If this isn't set, Array(resources) will return []
46
46
  # This is important, because jsonapi-serializable makes this call
47
+ # To see the test failure, remote resource position > department
47
48
  model.instance_variable_set(:@__graphiti_resource, 1)
48
49
  model.instance_variable_set(:@__graphiti_serializer, serializer)
49
50
  end
@@ -12,26 +12,48 @@ module Graphiti
12
12
  def apply
13
13
  return unless readable?
14
14
 
15
+ remove_guard if previously_guarded?
16
+
15
17
  if @name == :id
16
18
  @serializer.id(&proc)
17
- elsif @attr[:proc]
18
- @serializer.send(_method, @name, serializer_options, &proc)
19
- elsif @serializer.attribute_blocks[@name].nil?
19
+ elsif @attr[:proc] ||
20
+ !previously_applied? ||
21
+ previously_applied_via_resource?
20
22
  @serializer.send(_method, @name, serializer_options, &proc)
21
- else
22
- unless @serializer.send(applied_method).include?(@name)
23
- inner = @serializer.attribute_blocks.delete(@name)
24
- wrapped = wrap_proc(inner)
25
- @serializer.send(_method, @name, serializer_options, &wrapped)
26
- end
23
+ else # Previously applied via explicit serializer, so wrap it
24
+ inner = @serializer.attribute_blocks.delete(@name)
25
+ wrapped = wrap_proc(inner)
26
+ @serializer.send(_method, @name, serializer_options, &wrapped)
27
27
  end
28
28
 
29
29
  existing = @serializer.send(applied_method)
30
30
  @serializer.send(:"#{applied_method}=", [@name] | existing)
31
+
32
+ @serializer.meta do
33
+ if !!@resource.try(:cursor_paginatable?) && !Graphiti.context[:graphql]
34
+ {cursor: cursor}
35
+ end
36
+ end
31
37
  end
32
38
 
33
39
  private
34
40
 
41
+ def previously_applied?
42
+ @serializer.attribute_blocks[@name].present?
43
+ end
44
+
45
+ def previously_applied_via_resource?
46
+ @serializer.send(applied_method).include?(@name)
47
+ end
48
+
49
+ def previously_guarded?
50
+ @serializer.field_condition_blocks[@name]
51
+ end
52
+
53
+ def remove_guard
54
+ @serializer.field_condition_blocks.delete(@name)
55
+ end
56
+
35
57
  def applied_method
36
58
  if extra?
37
59
  :extra_attributes_applied_via_resource
@@ -56,16 +78,21 @@ module Graphiti
56
78
  method_name = @attr[:readable]
57
79
  instance = @resource.new
58
80
  attribute = @name.to_s
81
+ resource_class = @resource
59
82
 
60
83
  -> {
61
84
  method = instance.method(method_name)
62
- if method.arity.zero?
85
+ result = if method.arity.zero?
63
86
  instance.instance_eval(&method_name)
64
87
  elsif method.arity == 1
65
88
  instance.instance_exec(@object, &method)
66
89
  else
67
90
  instance.instance_exec(@object, attribute, &method)
68
91
  end
92
+ if Graphiti.context[:graphql] && !result
93
+ raise ::Graphiti::Errors::UnreadableAttribute.new(resource_class, attribute)
94
+ end
95
+ result
69
96
  }
70
97
  end
71
98
 
@@ -1,3 +1,3 @@
1
1
  module Graphiti
2
- VERSION = "1.2.31"
2
+ VERSION = "1.3.4"
3
3
  end
data/lib/graphiti.rb CHANGED
@@ -6,6 +6,7 @@ require "active_support/core_ext/class/attribute"
6
6
  require "active_support/core_ext/hash/conversions" # to_xml
7
7
  require "active_support/concern"
8
8
  require "active_support/time"
9
+ require "active_support/deprecation"
9
10
 
10
11
  require "dry-types"
11
12
  require "graphiti_errors"
@@ -141,6 +142,7 @@ require "graphiti/scoping/sort"
141
142
  require "graphiti/scoping/paginate"
142
143
  require "graphiti/scoping/extra_attributes"
143
144
  require "graphiti/scoping/filterable"
145
+ require "graphiti/scoping/filter_group_validator"
144
146
  require "graphiti/scoping/default_filter"
145
147
  require "graphiti/scoping/filter"
146
148
  require "graphiti/stats/dsl"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphiti
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.31
4
+ version: 1.3.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lee Richmond
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-11-02 00:00:00.000000000 Z
11
+ date: 2021-09-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jsonapi-serializable
@@ -308,6 +308,7 @@ files:
308
308
  - lib/graphiti/scoping/default_filter.rb
309
309
  - lib/graphiti/scoping/extra_attributes.rb
310
310
  - lib/graphiti/scoping/filter.rb
311
+ - lib/graphiti/scoping/filter_group_validator.rb
311
312
  - lib/graphiti/scoping/filterable.rb
312
313
  - lib/graphiti/scoping/paginate.rb
313
314
  - lib/graphiti/scoping/sort.rb
@@ -360,7 +361,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
360
361
  - !ruby/object:Gem::Version
361
362
  version: '0'
362
363
  requirements: []
363
- rubygems_version: 3.0.6
364
+ rubygems_version: 3.1.6
364
365
  signing_key:
365
366
  specification_version: 4
366
367
  summary: Easily build jsonapi.org-compatible APIs