graphiti 1.2.16 → 1.3.9

Sign up to get free protection for your applications and to get access to all the features.
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])