graphiti 1.2.16 → 1.3.9

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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +96 -0
  3. data/.standard.yml +4 -4
  4. data/Appraisals +23 -17
  5. data/CHANGELOG.md +7 -1
  6. data/Guardfile +5 -5
  7. data/deprecated_generators/graphiti/generator_mixin.rb +1 -0
  8. data/deprecated_generators/graphiti/resource_generator.rb +1 -1
  9. data/gemfiles/{rails_5.gemfile → rails_5_2.gemfile} +2 -2
  10. data/gemfiles/{rails_5_graphiti_rails.gemfile → rails_5_2_graphiti_rails.gemfile} +3 -4
  11. data/gemfiles/rails_6.gemfile +1 -1
  12. data/gemfiles/rails_6_graphiti_rails.gemfile +2 -3
  13. data/gemfiles/{rails_4.gemfile → rails_7.gemfile} +2 -2
  14. data/gemfiles/rails_7_graphiti_rails.gemfile +19 -0
  15. data/graphiti.gemspec +16 -16
  16. data/lib/graphiti/adapters/abstract.rb +20 -5
  17. data/lib/graphiti/adapters/active_record/belongs_to_sideload.rb +1 -1
  18. data/lib/graphiti/adapters/active_record/has_many_sideload.rb +1 -1
  19. data/lib/graphiti/adapters/active_record/has_one_sideload.rb +1 -1
  20. data/lib/graphiti/adapters/active_record/{inferrence.rb → inference.rb} +2 -2
  21. data/lib/graphiti/adapters/active_record/many_to_many_sideload.rb +19 -0
  22. data/lib/graphiti/adapters/active_record.rb +119 -74
  23. data/lib/graphiti/adapters/graphiti_api.rb +1 -1
  24. data/lib/graphiti/adapters/null.rb +1 -1
  25. data/lib/graphiti/adapters/persistence/associations.rb +78 -0
  26. data/lib/graphiti/configuration.rb +3 -1
  27. data/lib/graphiti/debugger.rb +12 -8
  28. data/lib/graphiti/delegates/pagination.rb +47 -13
  29. data/lib/graphiti/deserializer.rb +3 -3
  30. data/lib/graphiti/errors.rb +109 -15
  31. data/lib/graphiti/extensions/extra_attribute.rb +4 -4
  32. data/lib/graphiti/extensions/temp_id.rb +1 -1
  33. data/lib/graphiti/filter_operators.rb +0 -1
  34. data/lib/graphiti/hash_renderer.rb +198 -21
  35. data/lib/graphiti/query.rb +105 -73
  36. data/lib/graphiti/railtie.rb +5 -5
  37. data/lib/graphiti/renderer.rb +19 -1
  38. data/lib/graphiti/request_validator.rb +10 -10
  39. data/lib/graphiti/request_validators/update_validator.rb +4 -5
  40. data/lib/graphiti/request_validators/validator.rb +38 -24
  41. data/lib/graphiti/resource/configuration.rb +35 -7
  42. data/lib/graphiti/resource/dsl.rb +34 -8
  43. data/lib/graphiti/resource/interface.rb +13 -3
  44. data/lib/graphiti/resource/links.rb +3 -3
  45. data/lib/graphiti/resource/persistence.rb +2 -1
  46. data/lib/graphiti/resource/polymorphism.rb +8 -2
  47. data/lib/graphiti/resource/remote.rb +2 -2
  48. data/lib/graphiti/resource/sideloading.rb +4 -4
  49. data/lib/graphiti/resource.rb +12 -1
  50. data/lib/graphiti/resource_proxy.rb +23 -3
  51. data/lib/graphiti/runner.rb +5 -5
  52. data/lib/graphiti/schema.rb +36 -11
  53. data/lib/graphiti/schema_diff.rb +44 -4
  54. data/lib/graphiti/scope.rb +8 -10
  55. data/lib/graphiti/scoping/base.rb +3 -3
  56. data/lib/graphiti/scoping/filter.rb +36 -15
  57. data/lib/graphiti/scoping/filter_group_validator.rb +78 -0
  58. data/lib/graphiti/scoping/paginate.rb +47 -3
  59. data/lib/graphiti/scoping/sort.rb +5 -7
  60. data/lib/graphiti/serializer.rb +49 -7
  61. data/lib/graphiti/sideload/belongs_to.rb +1 -1
  62. data/lib/graphiti/sideload/has_many.rb +19 -1
  63. data/lib/graphiti/sideload/many_to_many.rb +11 -4
  64. data/lib/graphiti/sideload/polymorphic_belongs_to.rb +3 -4
  65. data/lib/graphiti/sideload.rb +47 -23
  66. data/lib/graphiti/stats/dsl.rb +0 -1
  67. data/lib/graphiti/stats/payload.rb +12 -9
  68. data/lib/graphiti/types.rb +15 -15
  69. data/lib/graphiti/util/attribute_check.rb +1 -1
  70. data/lib/graphiti/util/class.rb +6 -0
  71. data/lib/graphiti/util/link.rb +10 -2
  72. data/lib/graphiti/util/persistence.rb +21 -78
  73. data/lib/graphiti/util/relationship_payload.rb +4 -4
  74. data/lib/graphiti/util/remote_params.rb +9 -4
  75. data/lib/graphiti/util/remote_serializer.rb +1 -0
  76. data/lib/graphiti/util/serializer_attributes.rb +41 -11
  77. data/lib/graphiti/util/simple_errors.rb +4 -4
  78. data/lib/graphiti/util/transaction_hooks_recorder.rb +1 -1
  79. data/lib/graphiti/version.rb +1 -1
  80. data/lib/graphiti.rb +6 -3
  81. metadata +46 -37
  82. data/.travis.yml +0 -59
@@ -5,10 +5,15 @@ module Graphiti
5
5
 
6
6
  class_methods do
7
7
  def filter(name, *args, &blk)
8
+ name = name.to_sym
8
9
  opts = args.extract_options!
9
10
  type_override = args[0]
10
11
 
11
- if (att = get_attr(name, :filterable, raise_error: :only_unsupported))
12
+ if (att = (attributes[name] || extra_attributes[name]))
13
+ # We're opting in to filtering, so force this
14
+ # UNLESS the filter is guarded at the attribute level
15
+ att[:filterable] = true if att[:filterable] == false
16
+
12
17
  aliases = [name, opts[:aliases]].flatten.compact
13
18
  operators = FilterOperators.build(self, att[:type], opts, &blk)
14
19
 
@@ -22,6 +27,8 @@ module Graphiti
22
27
  end
23
28
 
24
29
  required = att[:filterable] == :required || !!opts[:required]
30
+ schema = !!opts[:via_attribute_dsl] ? att[:schema] : opts[:schema] != false
31
+
25
32
  config[:filters][name.to_sym] = {
26
33
  aliases: aliases,
27
34
  name: name.to_sym,
@@ -31,8 +38,10 @@ module Graphiti
31
38
  single: !!opts[:single],
32
39
  dependencies: opts[:dependent],
33
40
  required: required,
41
+ schema: schema,
34
42
  operators: operators.to_hash,
35
- allow_nil: opts.fetch(:allow_nil, filters_accept_nil_by_default)
43
+ allow_nil: opts.fetch(:allow_nil, filters_accept_nil_by_default),
44
+ deny_empty: opts.fetch(:deny_empty, filters_deny_empty_by_default)
36
45
  }
37
46
  elsif (type = args[0])
38
47
  attribute name, type, only: [:filterable], allow: opts[:allow]
@@ -42,8 +51,19 @@ module Graphiti
42
51
  end
43
52
  end
44
53
 
54
+ def filter_group(filter_names, *args)
55
+ opts = args.extract_options!
56
+
57
+ Scoping::FilterGroupValidator.raise_unless_filter_group_requirement_valid!(self, opts[:required])
58
+
59
+ config[:grouped_filters] = {
60
+ names: filter_names,
61
+ required: opts[:required]
62
+ }
63
+ end
64
+
45
65
  def sort_all(&blk)
46
- if block_given?
66
+ if blk
47
67
  config[:_sort_all] = blk
48
68
  else
49
69
  config[:_sort_all]
@@ -55,7 +75,7 @@ module Graphiti
55
75
 
56
76
  if get_attr(name, :sortable, raise_error: :only_unsupported)
57
77
  config[:sorts][name] = {
58
- proc: blk,
78
+ proc: blk
59
79
  }.merge(opts.slice(:only))
60
80
  elsif (type = args[0])
61
81
  attribute name, type, only: [:sortable]
@@ -78,7 +98,7 @@ module Graphiti
78
98
  def default_filter(name = nil, &blk)
79
99
  name ||= :__default
80
100
  config[:default_filters][name.to_sym] = {
81
- filter: blk,
101
+ filter: blk
82
102
  }
83
103
  end
84
104
 
@@ -117,7 +137,7 @@ module Graphiti
117
137
  options[:sortable] ? sort(name) : config[:sorts].delete(name)
118
138
 
119
139
  if options[:filterable]
120
- filter(name, allow: options[:allow])
140
+ filter(name, allow: options[:allow], via_attribute_dsl: true)
121
141
  else
122
142
  config[:filters].delete(name)
123
143
  end
@@ -132,8 +152,10 @@ module Graphiti
132
152
  writable: false,
133
153
  sortable: false,
134
154
  filterable: false,
155
+ schema: true
135
156
  }
136
157
  options = defaults.merge(options)
158
+ attribute_option(options, :readable)
137
159
  config[:extra_attributes][name] = options
138
160
  apply_extra_attributes_to_serializer
139
161
  end
@@ -146,6 +168,10 @@ module Graphiti
146
168
  end
147
169
  end
148
170
 
171
+ def link(name, &blk)
172
+ config[:links][name.to_sym] = blk
173
+ end
174
+
149
175
  def all_attributes
150
176
  attributes.merge(extra_attributes)
151
177
  end
@@ -163,9 +189,9 @@ module Graphiti
163
189
  def attribute_option(options, name, exclusive = false)
164
190
  if options[name] != false
165
191
  default = if (only = options[:only]) && !exclusive
166
- Array(only).include?(name) ? true : false
192
+ Array(only).include?(name)
167
193
  elsif (except = options[:except]) && !exclusive
168
- Array(except).include?(name) ? false : true
194
+ !Array(except).include?(name)
169
195
  else
170
196
  send(:"attributes_#{name}_by_default")
171
197
  end
@@ -11,7 +11,7 @@ module Graphiti
11
11
 
12
12
  # @api private
13
13
  def _all(params, opts, base_scope)
14
- runner = Runner.new(self, params, opts.delete(:query))
14
+ runner = Runner.new(self, params, opts.delete(:query), :all)
15
15
  opts[:params] = params
16
16
  runner.proxy(base_scope, opts)
17
17
  end
@@ -23,11 +23,14 @@ module Graphiti
23
23
 
24
24
  # @api private
25
25
  def _find(params = {}, base_scope = nil)
26
+ guard_nil_id!(params[:data])
27
+ guard_nil_id!(params)
28
+
26
29
  id = params[:data].try(:[], :id) || params.delete(:id)
27
30
  params[:filter] ||= {}
28
31
  params[:filter][:id] = id if id
29
32
 
30
- runner = Runner.new(self, params)
33
+ runner = Runner.new(self, params, nil, :find)
31
34
  runner.proxy base_scope,
32
35
  single: true,
33
36
  raise_on_missing: true,
@@ -43,7 +46,7 @@ module Graphiti
43
46
  private
44
47
 
45
48
  def validate!(params)
46
- return unless validate_endpoints?
49
+ return if Graphiti.context[:graphql] || !validate_endpoints?
47
50
 
48
51
  if context&.respond_to?(:request)
49
52
  path = context.request.env["PATH_INFO"]
@@ -52,6 +55,13 @@ module Graphiti
52
55
  end
53
56
  end
54
57
  end
58
+
59
+ def guard_nil_id!(params)
60
+ return unless params
61
+ if params.key?(:id) && params[:id].nil?
62
+ raise Errors::UndefinedIDLookup.new(self)
63
+ end
64
+ end
55
65
  end
56
66
  end
57
67
  end
@@ -39,7 +39,7 @@ module Graphiti
39
39
  path: path,
40
40
  full_path: full_path_for(path),
41
41
  url: url_for(path),
42
- actions: DEFAULT_ACTIONS.dup,
42
+ actions: DEFAULT_ACTIONS.dup
43
43
  }
44
44
  end
45
45
 
@@ -49,7 +49,7 @@ module Graphiti
49
49
  path: path,
50
50
  full_path: full_path_for(path),
51
51
  url: url_for(path),
52
- actions: actions,
52
+ actions: actions
53
53
  }
54
54
  end
55
55
 
@@ -60,7 +60,7 @@ module Graphiti
60
60
  path: path,
61
61
  full_path: full_path_for(path),
62
62
  url: url_for(path),
63
- actions: actions,
63
+ actions: actions
64
64
  }]
65
65
  end
66
66
 
@@ -89,7 +89,8 @@ module Graphiti
89
89
 
90
90
  def update(update_params, meta = nil)
91
91
  model_instance = nil
92
- id = update_params.delete(:id)
92
+ id = update_params[:id]
93
+ update_params = update_params.except(:id)
93
94
 
94
95
  run_callbacks :persistence, :update, update_params, meta do
95
96
  run_callbacks :attributes, :update, update_params, meta do |params|
@@ -42,9 +42,14 @@ module Graphiti
42
42
  end
43
43
 
44
44
  def sideload(name)
45
- sl = super
45
+ if (split_on = name.to_s.split(/^on__/)).length > 1
46
+ on_type, name = split_on[1].split("--").map(&:to_sym)
47
+ end
48
+
49
+ sl = super(name)
46
50
  if !polymorphic_child? && sl.nil?
47
51
  children.each do |c|
52
+ next if on_type && c.type != on_type
48
53
  break if (sl = c.sideloads[name])
49
54
  end
50
55
  end
@@ -67,7 +72,8 @@ module Graphiti
67
72
  end
68
73
 
69
74
  def resource_for_model(model)
70
- resource = children.find { |c| model.is_a?(c.model) }
75
+ resource = children.find { |c| model.instance_of?(c.model) } ||
76
+ children.find { |c| model.is_a?(c.model) }
71
77
  if resource.nil?
72
78
  raise Errors::PolymorphicResourceChildNotFound.new(self, model: model)
73
79
  else
@@ -19,7 +19,7 @@ module Graphiti
19
19
  end
20
20
 
21
21
  def save(model, meta)
22
- if meta[:attributes] == {} && meta[:method] == :update
22
+ if meta[:attributes].except(:id) == {} && meta[:method] == :update
23
23
  model
24
24
  else
25
25
  raise Errors::RemoteWrite.new(self.class)
@@ -31,7 +31,7 @@ module Graphiti
31
31
  end
32
32
 
33
33
  def before_resolve(scope, query)
34
- scope[:params] = Util::RemoteParams.generate(self, query)
34
+ scope[:params] = Util::RemoteParams.generate(self, query, scope[:foreign_key])
35
35
  scope
36
36
  end
37
37
 
@@ -68,10 +68,10 @@ module Graphiti
68
68
  model_ref = model
69
69
  has_many name, opts do
70
70
  params do |hash|
71
- hash[:filter][:"#{as}_type"] = model_ref.name
71
+ hash[:filter][:"#{as}_type"] = { eql: model_ref.name }
72
72
  end
73
73
 
74
- instance_eval(&blk) if block_given?
74
+ instance_eval(&blk) if blk
75
75
  end
76
76
  end
77
77
 
@@ -82,10 +82,10 @@ module Graphiti
82
82
  model_ref = model
83
83
  has_one name, opts do
84
84
  params do |hash|
85
- hash[:filter][:"#{as}_type"] = model_ref.name
85
+ hash[:filter][:"#{as}_type"] = { eql: model_ref.name }
86
86
  end
87
87
 
88
- instance_eval(&blk) if block_given?
88
+ instance_eval(&blk) if blk
89
89
  end
90
90
  end
91
91
 
@@ -28,11 +28,12 @@ module Graphiti
28
28
  serializer
29
29
  end
30
30
 
31
- def decorate_record(record)
31
+ def decorate_record(record, index = nil)
32
32
  unless record.instance_variable_get(:@__graphiti_serializer)
33
33
  serializer = serializer_for(record)
34
34
  record.instance_variable_set(:@__graphiti_serializer, serializer)
35
35
  record.instance_variable_set(:@__graphiti_resource, self)
36
+ record.instance_variable_set(:@__graphiti_index, index) if index
36
37
  end
37
38
  end
38
39
 
@@ -148,5 +149,15 @@ module Graphiti
148
149
  end
149
150
  response
150
151
  end
152
+
153
+ def links?
154
+ self.class.links.any?
155
+ end
156
+
157
+ def links(model)
158
+ self.class.links.each_with_object({}) do |(name, blk), memo|
159
+ memo[name] = instance_exec(model, &blk)
160
+ end
161
+ end
151
162
  end
152
163
  end
@@ -47,10 +47,22 @@ module Graphiti
47
47
  Renderer.new(self, options).to_json
48
48
  end
49
49
 
50
+ def as_json(options = {})
51
+ Renderer.new(self, options).as_json
52
+ end
53
+
50
54
  def to_xml(options = {})
51
55
  Renderer.new(self, options).to_xml
52
56
  end
53
57
 
58
+ def to_graphql(options = {})
59
+ Renderer.new(self, options).to_graphql
60
+ end
61
+
62
+ def as_graphql(options = {})
63
+ Renderer.new(self, options).as_graphql
64
+ end
65
+
54
66
  def data
55
67
  @data ||= begin
56
68
  records = @scope.resolve
@@ -61,7 +73,7 @@ module Graphiti
61
73
  records
62
74
  end
63
75
  end
64
- alias to_a data
76
+ alias_method :to_a, :data
65
77
 
66
78
  def meta
67
79
  @meta ||= data.respond_to?(:meta) ? data.meta : {}
@@ -73,9 +85,15 @@ module Graphiti
73
85
 
74
86
  def stats
75
87
  @stats ||= if @query.hash[:stats]
88
+ scope = @scope.unpaginated_object
89
+ if resource.adapter.can_group?
90
+ if (group = @query.hash[:stats].delete(:group_by))
91
+ scope = resource.adapter.group(scope, group[0])
92
+ end
93
+ end
76
94
  payload = Stats::Payload.new @resource,
77
95
  @query,
78
- @scope.unpaginated_object,
96
+ scope,
79
97
  data
80
98
  payload.generate
81
99
  else
@@ -93,7 +111,7 @@ module Graphiti
93
111
  original = Graphiti.context[:namespace]
94
112
  begin
95
113
  Graphiti.context[:namespace] = action
96
- ::Graphiti::RequestValidator.new(@resource, @payload.params).validate!
114
+ ::Graphiti::RequestValidator.new(@resource, @payload.params, action).validate!
97
115
  validator = persist {
98
116
  @resource.persist_with_relationships \
99
117
  @payload.meta(action: action),
@@ -118,6 +136,7 @@ module Graphiti
118
136
  end
119
137
 
120
138
  def destroy
139
+ data
121
140
  transaction_response = @resource.transaction do
122
141
  metadata = {method: :destroy}
123
142
  model = @resource.destroy(@query.filters[:id], metadata)
@@ -135,6 +154,7 @@ module Graphiti
135
154
  end
136
155
 
137
156
  def update_attributes
157
+ data
138
158
  save(action: :update)
139
159
  end
140
160
 
@@ -3,13 +3,13 @@ module Graphiti
3
3
  attr_reader :params
4
4
  attr_reader :deserialized_payload
5
5
 
6
- def initialize(resource_class, params, query = nil)
6
+ def initialize(resource_class, params, query = nil, action = nil)
7
7
  @resource_class = resource_class
8
8
  @params = params
9
9
  @query = query
10
+ @action = action
10
11
 
11
- validator = RequestValidator.new(jsonapi_resource, params)
12
-
12
+ validator = RequestValidator.new(jsonapi_resource, params, action)
13
13
  validator.validate!
14
14
 
15
15
  @deserialized_payload = validator.deserialized_payload
@@ -30,7 +30,7 @@ module Graphiti
30
30
  end
31
31
 
32
32
  def query
33
- @query ||= Query.new(jsonapi_resource, params)
33
+ @query ||= Query.new(jsonapi_resource, params, nil, nil, [], @action)
34
34
  end
35
35
 
36
36
  def query_hash
@@ -50,7 +50,7 @@ module Graphiti
50
50
  def jsonapi_render_options
51
51
  options = {}
52
52
  options.merge!(default_jsonapi_render_options)
53
- options[:meta] ||= {}
53
+ options[:meta] ||= {}
54
54
  options[:expose] ||= {}
55
55
  options[:expose][:context] = jsonapi_context
56
56
  options
@@ -7,6 +7,7 @@ module Graphiti
7
7
  ::Rails.application.eager_load! if defined?(::Rails)
8
8
  resources ||= Graphiti.resources.reject(&:abstract_class?)
9
9
  resources.reject! { |r| r.name.nil? }
10
+
10
11
  new(resources).generate
11
12
  end
12
13
 
@@ -25,7 +26,7 @@ module Graphiti
25
26
 
26
27
  def initialize(resources)
27
28
  @resources = resources.sort_by(&:name)
28
- @remote_resources = resources.select(&:remote?)
29
+ @remote_resources = @resources.select(&:remote?)
29
30
  @local_resources = @resources - @remote_resources
30
31
  end
31
32
 
@@ -33,7 +34,7 @@ module Graphiti
33
34
  {
34
35
  resources: generate_resources,
35
36
  endpoints: generate_endpoints,
36
- types: generate_types,
37
+ types: generate_types
37
38
  }
38
39
  end
39
40
 
@@ -89,14 +90,20 @@ module Graphiti
89
90
  config = {
90
91
  name: r.name,
91
92
  type: r.type.to_s,
93
+ graphql_entrypoint: r.graphql_entrypoint.to_s,
92
94
  description: r.description,
93
95
  attributes: attributes(r),
94
96
  extra_attributes: extra_attributes(r),
95
97
  sorts: sorts(r),
96
98
  filters: filters(r),
97
99
  relationships: relationships(r),
100
+ stats: stats(r)
98
101
  }
99
102
 
103
+ if r.grouped_filters.any?
104
+ config[:filter_group] = r.grouped_filters
105
+ end
106
+
100
107
  if r.default_sort
101
108
  default_sort = r.default_sort.map { |s|
102
109
  {s.keys.first.to_s => s.values.first.to_s}
@@ -108,7 +115,7 @@ module Graphiti
108
115
  config[:default_page_size] = r.default_page_size
109
116
  end
110
117
 
111
- if r.polymorphic?
118
+ if r.polymorphic? && !r.polymorphic_child?
112
119
  config[:polymorphic] = true
113
120
  config[:children] = r.children.map(&:name)
114
121
  end
@@ -121,7 +128,7 @@ module Graphiti
121
128
  name: r.name,
122
129
  description: r.description,
123
130
  remote: r.remote_url,
124
- relationships: relationships(r),
131
+ relationships: relationships(r)
125
132
  }
126
133
  }
127
134
 
@@ -136,7 +143,7 @@ module Graphiti
136
143
  type: config[:type].to_s,
137
144
  readable: flag(config[:readable]),
138
145
  writable: flag(config[:writable]),
139
- description: resource.attribute_description(name),
146
+ description: resource.attribute_description(name)
140
147
  }
141
148
  end
142
149
  end
@@ -146,10 +153,12 @@ module Graphiti
146
153
  def extra_attributes(resource)
147
154
  {}.tap do |attrs|
148
155
  resource.extra_attributes.each_pair do |name, config|
156
+ next unless config[:schema]
157
+
149
158
  attrs[name] = {
150
159
  type: config[:type].to_s,
151
160
  readable: flag(config[:readable]),
152
- description: resource.attribute_description(name),
161
+ description: resource.attribute_description(name)
153
162
  }
154
163
  end
155
164
  end
@@ -163,14 +172,22 @@ module Graphiti
163
172
  end
164
173
  end
165
174
 
175
+ def stats(resource)
176
+ {}.tap do |stats|
177
+ resource.stats.each_pair do |name, config|
178
+ stats[name] = config.calculations.keys
179
+ end
180
+ end
181
+ end
182
+
166
183
  def sorts(resource)
167
184
  {}.tap do |s|
168
185
  resource.sorts.each_pair do |name, sort|
169
- next unless resource.attributes[name][:schema]
186
+ attr = resource.all_attributes[name]
187
+ next unless attr[:schema]
170
188
 
171
189
  config = {}
172
190
  config[:only] = sort[:only] if sort[:only]
173
- attr = resource.attributes[name]
174
191
  if attr[:sortable].is_a?(Symbol)
175
192
  config[:guard] = true
176
193
  end
@@ -182,11 +199,11 @@ module Graphiti
182
199
  def filters(resource)
183
200
  {}.tap do |f|
184
201
  resource.filters.each_pair do |name, filter|
185
- next unless resource.attributes[name][:schema]
202
+ next unless resource.filters[name][:schema]
186
203
 
187
204
  config = {
188
205
  type: filter[:type].to_s,
189
- operators: filter[:operators].keys.map(&:to_s),
206
+ operators: filter[:operators].keys.map(&:to_s)
190
207
  }
191
208
 
192
209
  config[:single] = true if filter[:single]
@@ -194,7 +211,7 @@ module Graphiti
194
211
  config[:deny] = filter[:deny].map(&:to_s) if filter[:deny]
195
212
  config[:dependencies] = filter[:dependencies].map(&:to_s) if filter[:dependencies]
196
213
 
197
- attr = resource.attributes[name]
214
+ attr = resource.all_attributes[name]
198
215
  if attr[:filterable].is_a?(Symbol)
199
216
  if attr[:filterable] == :required
200
217
  config[:required] = true
@@ -202,11 +219,18 @@ module Graphiti
202
219
  config[:guard] = true
203
220
  end
204
221
  end
222
+ if filter[:required] # one-off filter, not attribute
223
+ config[:required] = true
224
+ end
205
225
  f[name] = config
206
226
  end
207
227
  end
208
228
  end
209
229
 
230
+ def filter_group(resource)
231
+ resource.config[:grouped_filters]
232
+ end
233
+
210
234
  def relationships(resource)
211
235
  {}.tap do |r|
212
236
  resource.sideloads.each_pair do |name, config|
@@ -214,6 +238,7 @@ module Graphiti
214
238
  if config.type == :polymorphic_belongs_to
215
239
  schema[:resources] = config.children.values
216
240
  .map(&:resource).map(&:class).map(&:name)
241
+ schema[:parent_resource] = config.parent_resource.class.name
217
242
  else
218
243
  schema[:resource] = config.resource.class.name
219
244
  end
@@ -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])