graphiti 1.2.32 → 1.2.37

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9ec84a0ed1957831fa1ae6bec24a6470d5bb16189681e096b5d59a468b95b49a
4
- data.tar.gz: 145633f22cad8f867ce295337a0b3bc5adfc45da6695b481abb092d0f7b17ed8
3
+ metadata.gz: 1ae9a52df4d54256b453573dc02f8eb2d8b1ad062eec7b38be89e3fbc7c6a9f3
4
+ data.tar.gz: 99ce6f9e0067eae81661338a933561b2c573b8a78bd92551d6ef5b21a16e6cdf
5
5
  SHA512:
6
- metadata.gz: a841e2eceebba1e495608ec2037a1c7deffa408fc7e408756a6edbfed0cfaded42d82abf55899724ec201a427a6f8226bbe554142739ff1067133d9d499835bd
7
- data.tar.gz: 3e6b741c6942c8b5ae89b08c7d5fdc6eb71b4c1b82794788207813bc914f0f245c43f196d603e2478c7dba61145403c507e1d7937028f8e217e71144beb2c128
6
+ metadata.gz: d77c8c268ef6c44b0557cf50fd3ecdfc71013fd25545cac6a935afd48f7e40caf47045af60649e03bf4f5ee01d8269d05f6be5991bc26a3c6303b7eb2d537afb
7
+ data.tar.gz: e83bf82263f8b59b2e10a243238c85204631f0eca55ed3306d07129f30d099421d4b518c841bf460187329760d0f9920ff90f5136797cfa1ed13d5cf18d90b3d
data/CHANGELOG.md CHANGED
@@ -1,6 +1,7 @@
1
1
  ## Unreleased
2
2
 
3
3
  Features:
4
+ - [329](https://github.com/graphiti-api/graphiti/pull/329) Propagate `extra_fields` to related resource links.
4
5
  - [242](https://github.com/graphiti-api/graphiti/pull/242) Bump `jsonapi-renderer` to `~0.2.2` now that (https://github.com/jsonapi-rb/jsonapi-renderer/pull/36) is fixed.
5
6
  - [158](https://github.com/graphiti-api/graphiti/pull/158) Filters options `allow_nil: true`
6
7
  Option can be set at the resource level `Resource.filters_accept_nil_by_default = true`.
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"
@@ -14,13 +14,12 @@ module Graphiti
14
14
  end
15
15
 
16
16
  class NullRelation
17
- extend ActiveModel::Naming
18
17
  attr_accessor :id, :errors, :pointer
19
18
 
20
19
  def initialize(id, pointer)
21
20
  @id = id
22
21
  @pointer = pointer
23
- @errors = ActiveModel::Errors.new(self)
22
+ @errors = Graphiti::Util::SimpleErrors.new(self)
24
23
  end
25
24
 
26
25
  def self.human_attribute_name(attr, options = {})
@@ -817,5 +816,34 @@ module Graphiti
817
816
 
818
817
  class ConflictRequest < InvalidRequest
819
818
  end
819
+
820
+ class FilterGroupInvalidRequirement < Base
821
+ def initialize(resource, valid_required_values)
822
+ @resource = resource
823
+ @valid_required_values = valid_required_values
824
+ end
825
+
826
+ def message
827
+ <<-MSG.gsub(/\s+/, " ").strip
828
+ The filter group required: value on resource #{@resource.class} must be one of the following:
829
+ #{@valid_required_values.join(", ")}
830
+ MSG
831
+ end
832
+ end
833
+
834
+ class FilterGroupMissingRequiredFilters < Base
835
+ def initialize(resource, filter_names, required)
836
+ @resource = resource
837
+ @filter_names = filter_names
838
+ @required_label = required == :all ? "All" : "One"
839
+ end
840
+
841
+ def message
842
+ <<-MSG.gsub(/\s+/, " ").strip
843
+ #{@required_label} of the following filters must be provided on resource #{@resource.type}:
844
+ #{@filter_names.join(", ")}
845
+ MSG
846
+ end
847
+ end
820
848
  end
821
849
  end
@@ -46,9 +46,9 @@ module Graphiti
46
46
  next false unless instance_exec(&options[:if])
47
47
  end
48
48
 
49
- @extra_fields &&
50
- @extra_fields[@_type] &&
51
- @extra_fields[@_type].include?(name)
49
+ next false unless @extra_fields
50
+
51
+ @extra_fields[@_type]&.include?(name) || @extra_fields[@resource&.type]&.include?(name)
52
52
  }
53
53
 
54
54
  attribute name, if: allow_field, &blk
@@ -74,19 +74,22 @@ module Graphiti
74
74
  name_chain << k unless name_chain.last == k
75
75
 
76
76
  unless remote_resource? && serializers.nil?
77
- attrs[name.to_sym] = if serializers.is_a?(Array)
78
- serializers.map do |rr|
77
+ payload = if serializers.is_a?(Array)
78
+ data = serializers.map { |rr|
79
79
  rr.to_hash(fields: fields, include: nested_include, graphql: graphql, name_chain: name_chain)
80
- end
80
+ }
81
+ graphql ? {nodes: data} : data
81
82
  elsif serializers.nil?
82
83
  if @resource.class.respond_to?(:sideload)
83
84
  if @resource.class.sideload(k).type.to_s.include?("_many")
84
- []
85
+ graphql ? {nodes: []} : []
85
86
  end
86
87
  end
87
88
  else
88
89
  serializers.to_hash(fields: fields, include: nested_include, graphql: graphql, name_chain: name_chain)
89
90
  end
91
+
92
+ attrs[name.to_sym] = payload
90
93
  end
91
94
  end
92
95
 
@@ -133,29 +136,58 @@ module Graphiti
133
136
  serializers = options[:data]
134
137
  opts = options.slice(:fields, :include)
135
138
  opts[:graphql] = @graphql
136
- to_hash(serializers, opts).tap do |hash|
137
- hash.merge!(options.slice(:meta)) unless options[:meta].empty?
138
- end
139
+ top_level_key = get_top_level_key(@resource, serializers.is_a?(Array))
140
+
141
+ hash = {top_level_key => {}}
142
+ nodes = get_nodes(serializers, opts)
143
+ add_nodes(hash, top_level_key, options, nodes, @graphql)
144
+ add_stats(hash, top_level_key, options, @graphql)
145
+ hash
139
146
  end
140
147
 
141
148
  private
142
149
 
143
- def to_hash(serializers, opts)
144
- {}.tap do |hash|
145
- top_level_key = :data
146
- if @graphql
147
- top_level_key = @resource.graphql_entrypoint
148
- unless serializers.is_a?(Array)
149
- top_level_key = top_level_key.to_s.singularize.to_sym
150
- end
150
+ def get_top_level_key(resource, is_many)
151
+ key = :data
152
+
153
+ if @graphql
154
+ key = @resource.graphql_entrypoint
155
+ key = key.to_s.singularize.to_sym unless is_many
156
+ end
157
+
158
+ key
159
+ end
160
+
161
+ def get_nodes(serializers, opts)
162
+ if serializers.is_a?(Array)
163
+ serializers.map do |s|
164
+ s.to_hash(**opts)
151
165
  end
166
+ else
167
+ serializers.to_hash(**opts)
168
+ end
169
+ end
170
+
171
+ def add_nodes(hash, top_level_key, opts, nodes, graphql)
172
+ payload = nodes
173
+ if graphql && nodes.is_a?(Array)
174
+ payload = {nodes: nodes}
175
+ end
152
176
 
153
- hash[top_level_key] = if serializers.is_a?(Array)
154
- serializers.map do |s|
155
- s.to_hash(**opts)
177
+ # Don't render nodes if we only requested stats
178
+ unless graphql && opts[:fields].values == [[:stats]]
179
+ hash[top_level_key] = payload
180
+ end
181
+ end
182
+
183
+ def add_stats(hash, top_level_key, options, graphql)
184
+ if options[:meta] && !options[:meta].empty?
185
+ if @graphql
186
+ if (stats = options[:meta][:stats])
187
+ hash[top_level_key][:stats] = stats
156
188
  end
157
189
  else
158
- serializers.to_hash(**opts)
190
+ hash.merge!(options.slice(:meta))
159
191
  end
160
192
  end
161
193
  end
@@ -26,17 +26,17 @@ module Graphiti
26
26
  [:data, :type],
27
27
  [:data, :id]
28
28
  ].each do |required_attr|
29
- attribute_mismatch(required_attr) unless @raw_params.dig(*required_attr)
29
+ attribute_mismatch(required_attr) unless @params.dig(*required_attr)
30
30
  end
31
31
  errors.blank?
32
32
  end
33
33
 
34
34
  def payload_matches_endpoint?
35
- unless @raw_params.dig(:data, :id) == @raw_params.dig(:filter, :id)
35
+ unless @params.dig(:data, :id) == @params.dig(:filter, :id)
36
36
  attribute_mismatch([:data, :id])
37
37
  end
38
38
 
39
- meta_type = @raw_params.dig(:data, :type)
39
+ meta_type = @params.dig(:data, :type)
40
40
 
41
41
  # NOTE: calling #to_s and comparing 2 strings is slower than
42
42
  # calling #to_sym and comparing 2 symbols. But pre ruby-2.2
@@ -5,21 +5,31 @@ module Graphiti
5
5
 
6
6
  def initialize(root_resource, raw_params, action)
7
7
  @root_resource = root_resource
8
- @raw_params = raw_params
8
+ @params = normalized_params(raw_params)
9
9
  @errors = Graphiti::Util::SimpleErrors.new(raw_params)
10
10
  @action = action
11
11
  end
12
12
 
13
13
  def validate
14
+ # Right now, all requests - even reads - go through the validator
15
+ # In the future these should have their own validation logic, but
16
+ # for now we can just bypass
17
+ return true unless @params.has_key?(:data)
18
+
14
19
  resource = @root_resource
15
- if (meta_type = deserialized_payload.meta[:type].try(:to_sym))
16
- if @root_resource.type != meta_type && @root_resource.polymorphic?
17
- resource = @root_resource.class.resource_for_type(meta_type).new
20
+
21
+ if @params[:data].has_key?(:type)
22
+ if (meta_type = deserialized_payload.meta[:type].try(:to_sym))
23
+ if @root_resource.type != meta_type && @root_resource.polymorphic?
24
+ resource = @root_resource.class.resource_for_type(meta_type).new
25
+ end
18
26
  end
19
- end
20
27
 
21
- typecast_attributes(resource, deserialized_payload.attributes, deserialized_payload.meta[:payload_path])
22
- process_relationships(resource, deserialized_payload.relationships, deserialized_payload.meta[:payload_path])
28
+ typecast_attributes(resource, deserialized_payload.attributes, deserialized_payload.meta[:payload_path])
29
+ process_relationships(resource, deserialized_payload.relationships, deserialized_payload.meta[:payload_path])
30
+ else
31
+ errors.add(:"data.type", :missing)
32
+ end
23
33
 
24
34
  errors.blank?
25
35
  end
@@ -33,14 +43,7 @@ module Graphiti
33
43
  end
34
44
 
35
45
  def deserialized_payload
36
- @deserialized_payload ||= begin
37
- payload = normalized_params
38
- if payload[:data] && payload[:data][:type]
39
- Graphiti::Deserializer.new(payload)
40
- else
41
- Graphiti::Deserializer.new({})
42
- end
43
- end
46
+ @deserialized_payload ||= Graphiti::Deserializer.new(@params)
44
47
  end
45
48
 
46
49
  private
@@ -86,8 +89,8 @@ module Graphiti
86
89
  end
87
90
  end
88
91
 
89
- def normalized_params
90
- normalized = @raw_params
92
+ def normalized_params(raw_params)
93
+ normalized = raw_params
91
94
  if normalized.respond_to?(:to_unsafe_h)
92
95
  normalized = normalized.to_unsafe_h.deep_symbolize_keys
93
96
  end
@@ -28,6 +28,14 @@ module Graphiti
28
28
  end
29
29
  end
30
30
 
31
+ def graphql_entrypoint=(val)
32
+ if val
33
+ super(val.to_s.camelize(:lower).to_sym)
34
+ else
35
+ super
36
+ end
37
+ end
38
+
31
39
  # The .stat call stores a proc based on adapter
32
40
  # So if we assign a new adapter, reconfigure
33
41
  def adapter=(val)
@@ -191,6 +199,7 @@ module Graphiti
191
199
  @config ||=
192
200
  {
193
201
  filters: {},
202
+ grouped_filters: {},
194
203
  default_filters: {},
195
204
  stats: {},
196
205
  sort_all: nil,
@@ -227,6 +236,10 @@ module Graphiti
227
236
  config[:filters]
228
237
  end
229
238
 
239
+ def grouped_filters
240
+ config[:grouped_filters]
241
+ end
242
+
230
243
  def sorts
231
244
  config[:sorts]
232
245
  end
@@ -265,6 +278,10 @@ module Graphiti
265
278
  self.class.filters
266
279
  end
267
280
 
281
+ def grouped_filters
282
+ self.class.grouped_filters
283
+ end
284
+
268
285
  def sort_all
269
286
  self.class.sort_all
270
287
  end
@@ -44,6 +44,17 @@ module Graphiti
44
44
  end
45
45
  end
46
46
 
47
+ def filter_group(filter_names, *args)
48
+ opts = args.extract_options!
49
+
50
+ Scoping::FilterGroupValidator.raise_unless_filter_group_requirement_valid!(self, opts[:required])
51
+
52
+ config[:grouped_filters] = {
53
+ names: filter_names,
54
+ required: opts[:required]
55
+ }
56
+ end
57
+
47
58
  def sort_all(&blk)
48
59
  if block_given?
49
60
  config[:_sort_all] = blk
@@ -96,9 +96,14 @@ module Graphiti
96
96
  extra_attributes: extra_attributes(r),
97
97
  sorts: sorts(r),
98
98
  filters: filters(r),
99
- relationships: relationships(r)
99
+ relationships: relationships(r),
100
+ stats: stats(r)
100
101
  }
101
102
 
103
+ if r.grouped_filters.any?
104
+ config[:filter_group] = r.grouped_filters
105
+ end
106
+
102
107
  if r.default_sort
103
108
  default_sort = r.default_sort.map { |s|
104
109
  {s.keys.first.to_s => s.values.first.to_s}
@@ -165,6 +170,14 @@ module Graphiti
165
170
  end
166
171
  end
167
172
 
173
+ def stats(resource)
174
+ {}.tap do |stats|
175
+ resource.stats.each_pair do |name, config|
176
+ stats[name] = config.calculations.keys
177
+ end
178
+ end
179
+ end
180
+
168
181
  def sorts(resource)
169
182
  {}.tap do |s|
170
183
  resource.sorts.each_pair do |name, sort|
@@ -212,6 +225,10 @@ module Graphiti
212
225
  end
213
226
  end
214
227
 
228
+ def filter_group(resource)
229
+ resource.config[:grouped_filters]
230
+ end
231
+
215
232
  def relationships(resource)
216
233
  {}.tap do |r|
217
234
  resource.sideloads.each_pair do |name, config|
@@ -1,8 +1,8 @@
1
1
  module Graphiti
2
2
  class SchemaDiff
3
3
  def initialize(old, new)
4
- @old = old.deep_symbolize_keys
5
- @new = new.deep_symbolize_keys
4
+ @old = JSON.parse(old.to_json).deep_symbolize_keys
5
+ @new = JSON.parse(new.to_json).deep_symbolize_keys
6
6
  @errors = []
7
7
  end
8
8
 
@@ -30,6 +30,8 @@ module Graphiti
30
30
  compare_extra_attributes(r, new_resource)
31
31
  compare_sorts(r, new_resource)
32
32
  compare_filters(r, new_resource)
33
+ compare_filter_group(r, new_resource)
34
+ compare_stats(r, new_resource)
33
35
  compare_relationships(r, new_resource)
34
36
  end
35
37
  end
@@ -133,12 +135,12 @@ module Graphiti
133
135
  end
134
136
 
135
137
  if new_sort[:only] && !old_sort[:only]
136
- @errors << "#{old_resource[:name]}: sort #{name.inspect} now limited to only #{new_sort[:only].inspect}."
138
+ @errors << "#{old_resource[:name]}: sort #{name.inspect} now limited to only #{new_sort[:only].to_sym.inspect}."
137
139
  end
138
140
 
139
141
  if new_sort[:only] && old_sort[:only]
140
142
  if new_sort[:only] != old_sort[:only]
141
- @errors << "#{old_resource[:name]}: sort #{name.inspect} was limited to only #{old_sort[:only].inspect}, now limited to only #{new_sort[:only].inspect}."
143
+ @errors << "#{old_resource[:name]}: sort #{name.inspect} was limited to only #{old_sort[:only].to_sym.inspect}, now limited to only #{new_sort[:only].to_sym.inspect}."
142
144
  end
143
145
  end
144
146
  end
@@ -204,6 +206,44 @@ module Graphiti
204
206
  end
205
207
  end
206
208
 
209
+ def compare_filter_group(old_resource, new_resource)
210
+ if new_resource[:filter_group]
211
+ if old_resource[:filter_group]
212
+ new_names = new_resource[:filter_group][:names]
213
+ old_names = old_resource[:filter_group][:names]
214
+ diff = new_names - old_names
215
+ if !diff.empty? && new_resource[:filter_group][:required] == "all"
216
+ @errors << "#{old_resource[:name]}: all required filter group #{old_names.map(&:to_sym).inspect} added #{"member".pluralize(diff.length)} #{diff.map(&:to_sym).inspect}."
217
+ end
218
+
219
+ old_required = old_resource[:filter_group][:required]
220
+ new_required = new_resource[:filter_group][:required]
221
+ if old_required == "any" && new_required == "all"
222
+ @errors << "#{old_resource[:name]}: filter group #{old_names.map(&:to_sym).inspect} moved from required: :any to required: :all"
223
+ end
224
+ else
225
+ @errors << "#{old_resource[:name]}: filter group #{new_resource[:filter_group][:names].map(&:to_sym).inspect} was added."
226
+ end
227
+ end
228
+ end
229
+
230
+ def compare_stats(old_resource, new_resource)
231
+ return unless old_resource.key?(:stats)
232
+
233
+ old_resource[:stats].each_pair do |name, old_calculations|
234
+ new_calculations = new_resource[:stats][name]
235
+ if new_calculations
236
+ old_calculations.each do |calc|
237
+ unless new_calculations.include?(calc)
238
+ @errors << "#{old_resource[:name]}: calculation #{calc.to_sym.inspect} was removed from stat #{name.inspect}."
239
+ end
240
+ end
241
+ else
242
+ @errors << "#{old_resource[:name]}: stat #{name.inspect} was removed."
243
+ end
244
+ end
245
+ end
246
+
207
247
  def compare_endpoints
208
248
  @old[:endpoints].each_pair do |path, old_endpoint|
209
249
  unless (new_endpoint = @new[:endpoints][path])
@@ -3,6 +3,13 @@ module Graphiti
3
3
  include Scoping::Filterable
4
4
 
5
5
  def apply
6
+ unless @opts[:bypass_required_filters]
7
+ Graphiti::Scoping::FilterGroupValidator.new(
8
+ resource,
9
+ query_hash
10
+ ).raise_unless_filter_group_requirements_met!
11
+ end
12
+
6
13
  if missing_required_filters.any? && !@opts[:bypass_required_filters]
7
14
  raise Errors::RequiredFilter.new(resource, missing_required_filters)
8
15
  end
@@ -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
@@ -136,6 +136,14 @@ module Graphiti
136
136
  base_filter(parents)
137
137
  end
138
138
 
139
+ def link_extra_fields
140
+ extra_fields_name = [association_name, resource.type].find { |param|
141
+ context.params.dig(:extra_fields, param)
142
+ }
143
+
144
+ {resource.type => context.params.dig(:extra_fields, extra_fields_name)} if extra_fields_name
145
+ end
146
+
139
147
  # The parent resource is a remote,
140
148
  # AND the sideload is a remote to the same endpoint
141
149
  def shared_remote?
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Graphiti
2
- VERSION = "1.2.32"
2
+ VERSION = "1.2.37"
3
3
  end
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.32
4
+ version: 1.2.37
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lee Richmond
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-02-28 00:00:00.000000000 Z
11
+ date: 2021-03-20 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