graphiti-rb 1.0.alpha.1
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 +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +20 -0
- data/.yardopts +2 -0
- data/Appraisals +11 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +12 -0
- data/Guardfile +32 -0
- data/LICENSE.txt +21 -0
- data/README.md +75 -0
- data/Rakefile +15 -0
- data/bin/appraisal +17 -0
- data/bin/console +14 -0
- data/bin/rspec +17 -0
- data/bin/setup +8 -0
- data/gemfiles/rails_4.gemfile +17 -0
- data/gemfiles/rails_5.gemfile +17 -0
- data/graphiti.gemspec +34 -0
- data/lib/generators/jsonapi/resource_generator.rb +169 -0
- data/lib/generators/jsonapi/templates/application_resource.rb.erb +15 -0
- data/lib/generators/jsonapi/templates/controller.rb.erb +61 -0
- data/lib/generators/jsonapi/templates/create_request_spec.rb.erb +30 -0
- data/lib/generators/jsonapi/templates/destroy_request_spec.rb.erb +20 -0
- data/lib/generators/jsonapi/templates/index_request_spec.rb.erb +22 -0
- data/lib/generators/jsonapi/templates/resource.rb.erb +11 -0
- data/lib/generators/jsonapi/templates/resource_reads_spec.rb.erb +62 -0
- data/lib/generators/jsonapi/templates/resource_writes_spec.rb.erb +63 -0
- data/lib/generators/jsonapi/templates/show_request_spec.rb.erb +21 -0
- data/lib/generators/jsonapi/templates/update_request_spec.rb.erb +34 -0
- data/lib/graphiti-rb.rb +1 -0
- data/lib/graphiti.rb +121 -0
- data/lib/graphiti/adapters/abstract.rb +516 -0
- data/lib/graphiti/adapters/active_record.rb +6 -0
- data/lib/graphiti/adapters/active_record/base.rb +249 -0
- data/lib/graphiti/adapters/active_record/belongs_to_sideload.rb +17 -0
- data/lib/graphiti/adapters/active_record/has_many_sideload.rb +17 -0
- data/lib/graphiti/adapters/active_record/has_one_sideload.rb +17 -0
- data/lib/graphiti/adapters/active_record/inferrence.rb +12 -0
- data/lib/graphiti/adapters/active_record/many_to_many_sideload.rb +30 -0
- data/lib/graphiti/adapters/null.rb +236 -0
- data/lib/graphiti/base.rb +70 -0
- data/lib/graphiti/configuration.rb +21 -0
- data/lib/graphiti/context.rb +16 -0
- data/lib/graphiti/deserializer.rb +208 -0
- data/lib/graphiti/errors.rb +309 -0
- data/lib/graphiti/extensions/boolean_attribute.rb +33 -0
- data/lib/graphiti/extensions/extra_attribute.rb +70 -0
- data/lib/graphiti/extensions/temp_id.rb +26 -0
- data/lib/graphiti/filter_operators.rb +25 -0
- data/lib/graphiti/hash_renderer.rb +57 -0
- data/lib/graphiti/jsonapi_serializable_ext.rb +50 -0
- data/lib/graphiti/query.rb +251 -0
- data/lib/graphiti/rails.rb +28 -0
- data/lib/graphiti/railtie.rb +74 -0
- data/lib/graphiti/renderer.rb +60 -0
- data/lib/graphiti/resource.rb +110 -0
- data/lib/graphiti/resource/configuration.rb +239 -0
- data/lib/graphiti/resource/dsl.rb +138 -0
- data/lib/graphiti/resource/interface.rb +32 -0
- data/lib/graphiti/resource/polymorphism.rb +68 -0
- data/lib/graphiti/resource/sideloading.rb +102 -0
- data/lib/graphiti/resource_proxy.rb +127 -0
- data/lib/graphiti/responders.rb +19 -0
- data/lib/graphiti/runner.rb +25 -0
- data/lib/graphiti/scope.rb +98 -0
- data/lib/graphiti/scoping/base.rb +99 -0
- data/lib/graphiti/scoping/default_filter.rb +58 -0
- data/lib/graphiti/scoping/extra_attributes.rb +29 -0
- data/lib/graphiti/scoping/filter.rb +93 -0
- data/lib/graphiti/scoping/filterable.rb +36 -0
- data/lib/graphiti/scoping/paginate.rb +87 -0
- data/lib/graphiti/scoping/sort.rb +64 -0
- data/lib/graphiti/sideload.rb +281 -0
- data/lib/graphiti/sideload/belongs_to.rb +34 -0
- data/lib/graphiti/sideload/has_many.rb +16 -0
- data/lib/graphiti/sideload/has_one.rb +9 -0
- data/lib/graphiti/sideload/many_to_many.rb +24 -0
- data/lib/graphiti/sideload/polymorphic_belongs_to.rb +108 -0
- data/lib/graphiti/stats/dsl.rb +89 -0
- data/lib/graphiti/stats/payload.rb +49 -0
- data/lib/graphiti/types.rb +172 -0
- data/lib/graphiti/util/attribute_check.rb +88 -0
- data/lib/graphiti/util/field_params.rb +16 -0
- data/lib/graphiti/util/hash.rb +51 -0
- data/lib/graphiti/util/hooks.rb +33 -0
- data/lib/graphiti/util/include_params.rb +39 -0
- data/lib/graphiti/util/persistence.rb +219 -0
- data/lib/graphiti/util/relationship_payload.rb +64 -0
- data/lib/graphiti/util/serializer_attributes.rb +97 -0
- data/lib/graphiti/util/sideload.rb +33 -0
- data/lib/graphiti/util/validation_response.rb +78 -0
- data/lib/graphiti/version.rb +3 -0
- metadata +317 -0
@@ -0,0 +1,19 @@
|
|
1
|
+
# If you're using Rails + responders gem to get respond_with
|
2
|
+
module Graphiti
|
3
|
+
module Responders
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
include ActionController::MimeResponds
|
8
|
+
respond_to(*Graphiti.config.respond_to)
|
9
|
+
end
|
10
|
+
|
11
|
+
# Override to avoid location url generation (for now)
|
12
|
+
def respond_with(*args, &blk)
|
13
|
+
opts = args.extract_options!
|
14
|
+
opts[:location] = nil
|
15
|
+
args << opts
|
16
|
+
super(*args, &blk)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Graphiti
|
2
|
+
class Runner
|
3
|
+
attr_reader :params
|
4
|
+
include Graphiti::Base
|
5
|
+
|
6
|
+
def initialize(resource_class, params)
|
7
|
+
@resource_class = resource_class
|
8
|
+
@params = params
|
9
|
+
end
|
10
|
+
|
11
|
+
def jsonapi_resource
|
12
|
+
@jsonapi_resource ||= @resource_class.new
|
13
|
+
end
|
14
|
+
|
15
|
+
# Typically, this is 'self' of a controller
|
16
|
+
# We're overriding here so we can do stuff like
|
17
|
+
#
|
18
|
+
# Graphiti.with_context my_context, {} do
|
19
|
+
# Runner.new ...
|
20
|
+
# end
|
21
|
+
def jsonapi_context
|
22
|
+
Graphiti.context[:object]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module Graphiti
|
2
|
+
class Scope
|
3
|
+
attr_accessor :object, :unpaginated_object
|
4
|
+
|
5
|
+
def initialize(object, resource, query, opts = {})
|
6
|
+
@object = object
|
7
|
+
@resource = resource
|
8
|
+
@query = query
|
9
|
+
@opts = opts
|
10
|
+
|
11
|
+
@object = @resource.around_scoping(@object, query_hash) do |scope|
|
12
|
+
apply_scoping(scope, opts)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def resolve_stats
|
17
|
+
if query_hash[:stats]
|
18
|
+
Stats::Payload.new(@resource, @query, @unpaginated_object).generate
|
19
|
+
else
|
20
|
+
{}
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def resolve
|
25
|
+
if @query.zero_results?
|
26
|
+
[]
|
27
|
+
else
|
28
|
+
resolved = @resource.resolve(@object)
|
29
|
+
assign_serializer(resolved)
|
30
|
+
yield resolved if block_given?
|
31
|
+
if @opts[:after_resolve]
|
32
|
+
@opts[:after_resolve].call(resolved)
|
33
|
+
end
|
34
|
+
sideload(resolved) unless @query.sideloads.empty?
|
35
|
+
resolved
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def query_hash
|
40
|
+
@query_hash ||= @query.to_hash
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
# Used to ensure the resource's serializer is used
|
46
|
+
# Not one derived through the usual jsonapi-rb logic
|
47
|
+
def assign_serializer(records)
|
48
|
+
records.each do |r|
|
49
|
+
serializer = @resource.serializer_for(r)
|
50
|
+
r.instance_variable_set(:@__serializer_klass, serializer)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def sideload(results)
|
55
|
+
return if results == []
|
56
|
+
|
57
|
+
concurrent = ::Graphiti.config.concurrency
|
58
|
+
promises = []
|
59
|
+
|
60
|
+
@query.sideloads.each_pair do |name, q|
|
61
|
+
sideload = @resource.class.sideload(name)
|
62
|
+
resolve_sideload = -> { sideload.resolve(results, q) }
|
63
|
+
if concurrent
|
64
|
+
promises << Concurrent::Promise.execute(&resolve_sideload)
|
65
|
+
else
|
66
|
+
resolve_sideload.call
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
if concurrent
|
71
|
+
# Wait for all promises to finish
|
72
|
+
while !promises.all? { |p| p.fulfilled? || p.rejected? }
|
73
|
+
sleep 0.01
|
74
|
+
end
|
75
|
+
# Re-raise the error with correct stacktrace
|
76
|
+
# OPTION** to avoid failing here?? if so need serializable patch
|
77
|
+
# to avoid loading data when association not loaded
|
78
|
+
if rejected = promises.find(&:rejected?)
|
79
|
+
raise rejected.reason
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def apply_scoping(scope, opts)
|
85
|
+
@object = scope
|
86
|
+
add_scoping(nil, Graphiti::Scoping::DefaultFilter, opts)
|
87
|
+
add_scoping(:filter, Graphiti::Scoping::Filter, opts)
|
88
|
+
add_scoping(:sort, Graphiti::Scoping::Sort, opts)
|
89
|
+
add_scoping(:paginate, Graphiti::Scoping::Paginate, opts)
|
90
|
+
@object
|
91
|
+
end
|
92
|
+
|
93
|
+
def add_scoping(key, scoping_class, opts, default = {})
|
94
|
+
@object = scoping_class.new(@resource, query_hash, @object, opts).apply
|
95
|
+
@unpaginated_object = @object unless key == :paginate
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module Graphiti
|
2
|
+
module Scoping
|
3
|
+
# The interface for scoping logic (filter, paginate, etc).
|
4
|
+
#
|
5
|
+
# This class defines some common behavior, such as falling back on
|
6
|
+
# a default if not part of the user request.
|
7
|
+
#
|
8
|
+
# @attr_reader [Resource] resource The corresponding Resource instance
|
9
|
+
# @attr_reader [Hash] query_hash the Query#to_hash node relevant to the current resource
|
10
|
+
#
|
11
|
+
# @see Scoping::DefaultFilter
|
12
|
+
# @see Scoping::ExtraFields
|
13
|
+
# @see Scoping::Filter
|
14
|
+
# @see Scoping::Paginate
|
15
|
+
# @see Scoping::Sort
|
16
|
+
# @see Scope#initialize
|
17
|
+
# @see Scope#query_hash
|
18
|
+
# @see Query#to_hash
|
19
|
+
class Base
|
20
|
+
attr_reader :resource, :query_hash
|
21
|
+
|
22
|
+
# @param [Resource] resource the Resource instance
|
23
|
+
# @param [Hash] query_hash the Query#to_hash node relevant to the current resource
|
24
|
+
# @param scope the base scope object to chain/modify
|
25
|
+
# @param [Hash] opts configuration options used by subclasses
|
26
|
+
def initialize(resource, query_hash, scope, opts = {})
|
27
|
+
@query_hash = query_hash
|
28
|
+
@resource = resource
|
29
|
+
@scope = scope
|
30
|
+
@opts = opts
|
31
|
+
end
|
32
|
+
|
33
|
+
# Apply this scoping criteria.
|
34
|
+
# This is where we would chain on pagination, sorting, etc.
|
35
|
+
#
|
36
|
+
# If #apply? returns false, does nothing. Otherwise will apply
|
37
|
+
# the default logic:
|
38
|
+
#
|
39
|
+
# # no block, run default logic via adapter
|
40
|
+
# allow_filter :name
|
41
|
+
#
|
42
|
+
# Or the customized proc:
|
43
|
+
#
|
44
|
+
# allow_filter :name do |scope, value|
|
45
|
+
# scope.where("upper(name) = ?", value.upcase)
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# @see #apply?
|
49
|
+
# @return the scope object
|
50
|
+
def apply
|
51
|
+
if apply?
|
52
|
+
apply_standard_or_override
|
53
|
+
else
|
54
|
+
@scope
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Should we process this scope logic?
|
59
|
+
#
|
60
|
+
# Useful for when we want to explicitly opt-out on
|
61
|
+
# certain requests, or avoid a default in certain contexts.
|
62
|
+
#
|
63
|
+
# @return [Boolean] if we should apply this scope logic
|
64
|
+
def apply?
|
65
|
+
true
|
66
|
+
end
|
67
|
+
|
68
|
+
# Defines how to call/apply the default scoping logic
|
69
|
+
def apply_standard_scope
|
70
|
+
raise 'override in subclass'
|
71
|
+
end
|
72
|
+
|
73
|
+
# Defines how to call/apply the custom scoping logic provided by the
|
74
|
+
# user.
|
75
|
+
def apply_custom_scope
|
76
|
+
raise 'override in subclass'
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
# If the user customized (by providing a block in the Resource DSL)
|
82
|
+
# then call the custom proc. Else, call the default proc.
|
83
|
+
def apply_standard_or_override
|
84
|
+
if apply_standard_scope?
|
85
|
+
@scope = apply_standard_scope
|
86
|
+
else
|
87
|
+
@scope = apply_custom_scope
|
88
|
+
end
|
89
|
+
|
90
|
+
@scope
|
91
|
+
end
|
92
|
+
|
93
|
+
# Should we apply the default proc, or a custom one?
|
94
|
+
def apply_standard_scope?
|
95
|
+
custom_scope.nil?
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Graphiti
|
2
|
+
# Default filters apply to every request, unless specifically overridden in
|
3
|
+
# the request.
|
4
|
+
#
|
5
|
+
# Maybe we only want to show active posts:
|
6
|
+
#
|
7
|
+
# class PostResource < ApplicationResource
|
8
|
+
# # ... code ...
|
9
|
+
# default_filter :active do |scope|
|
10
|
+
# scope.where(active: true)
|
11
|
+
# end
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# But if the user is an admin and specifically requests inactive posts:
|
15
|
+
#
|
16
|
+
# class PostResource < ApplicationResource
|
17
|
+
# # ... code ...
|
18
|
+
# allow_filter :active, if: admin?
|
19
|
+
#
|
20
|
+
# default_filter :active do |scope|
|
21
|
+
# scope.where(active: true)
|
22
|
+
# end
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# # Now a GET /posts?filter[active]=false will return inactive posts
|
26
|
+
# # if the user is an admin.
|
27
|
+
#
|
28
|
+
# @see Resource.default_filter
|
29
|
+
# @see Resource.allow_filter
|
30
|
+
class Scoping::DefaultFilter < Scoping::Base
|
31
|
+
include Scoping::Filterable
|
32
|
+
|
33
|
+
# Apply the default filtering logic.
|
34
|
+
# Loop through each defined default filter, and apply the default
|
35
|
+
# proc unless an explicit override is requested
|
36
|
+
#
|
37
|
+
# @return scope the scope object we are chaining/modifying
|
38
|
+
def apply
|
39
|
+
resource.default_filters.each_pair do |name, opts|
|
40
|
+
next if overridden?(name)
|
41
|
+
@scope = opts[:filter].call(@scope, resource.context)
|
42
|
+
end
|
43
|
+
|
44
|
+
@scope
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def overridden?(name)
|
50
|
+
if found = find_filter(name)
|
51
|
+
found_aliases = found[name][:aliases]
|
52
|
+
filter_param.keys.any? { |k| found_aliases.include?(k.to_sym) }
|
53
|
+
else
|
54
|
+
false
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Graphiti
|
2
|
+
class Scoping::ExtraAttributes < Scoping::Base
|
3
|
+
# Loop through all requested extra fields. If custom scoping
|
4
|
+
# logic is define for that field, run it. Otherwise, do nothing.
|
5
|
+
#
|
6
|
+
# @return the scope object we are chaining/modofying
|
7
|
+
def apply
|
8
|
+
each_extra_attribute do |callable|
|
9
|
+
@scope = callable.call(@scope, resource.context)
|
10
|
+
end
|
11
|
+
|
12
|
+
@scope
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def each_extra_field
|
18
|
+
resource.extra_fields.each_pair do |name, callable|
|
19
|
+
if extra_fields.include?(name)
|
20
|
+
yield callable
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def extra_fields
|
26
|
+
query_hash[:extra_fields][resource.type] || []
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module Graphiti
|
2
|
+
# Apply filtering logic to the scope
|
3
|
+
#
|
4
|
+
# If the user requests to filter a field that has not been whitelisted,
|
5
|
+
# a +Graphiti::Errors::BadFilter+ error will be raised.
|
6
|
+
#
|
7
|
+
# allow_filter :title # :title now whitelisted
|
8
|
+
#
|
9
|
+
# If the user requests a filter field that has been whitelisted, but
|
10
|
+
# does not pass the associated `+:if+ clause, +BadFilter+ will be raised.
|
11
|
+
#
|
12
|
+
# allow_filter :title, if: :admin?
|
13
|
+
#
|
14
|
+
# This will also honor filter aliases.
|
15
|
+
#
|
16
|
+
# # GET /posts?filter[headline]=foo will filter on title
|
17
|
+
# allow_filter :title, aliases: [:headline]
|
18
|
+
#
|
19
|
+
# @see Adapters::Abstract#filter
|
20
|
+
# @see Adapters::ActiveRecord#filter
|
21
|
+
# @see Resource.allow_filter
|
22
|
+
class Scoping::Filter < Scoping::Base
|
23
|
+
include Scoping::Filterable
|
24
|
+
|
25
|
+
# Apply the filtering logic.
|
26
|
+
#
|
27
|
+
# Loop and parse all requested filters, taking into account guards and
|
28
|
+
# aliases. If valid, call either the default or custom filtering logic.
|
29
|
+
# @return the scope we are chaining/modifying
|
30
|
+
def apply
|
31
|
+
if missing_required_filters.any?
|
32
|
+
raise Errors::RequiredFilter.new(resource, missing_required_filters)
|
33
|
+
end
|
34
|
+
|
35
|
+
each_filter do |filter, operator, value|
|
36
|
+
@scope = filter_scope(filter, operator, value)
|
37
|
+
end
|
38
|
+
|
39
|
+
@scope
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def filter_scope(filter, operator, value)
|
45
|
+
operator = operator.to_s.gsub('!', 'not_').to_sym
|
46
|
+
|
47
|
+
if custom_scope = filter.values.first[operator]
|
48
|
+
custom_scope.call(@scope, value, resource.context)
|
49
|
+
else
|
50
|
+
filter_via_adapter(filter, operator, value)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def filter_via_adapter(filter, operator, value)
|
55
|
+
type_name = Types.name_for(filter.values.first[:type])
|
56
|
+
method = :"filter_#{type_name}_#{operator}"
|
57
|
+
attribute = filter.keys.first
|
58
|
+
|
59
|
+
if resource.adapter.respond_to?(method)
|
60
|
+
resource.adapter.send(method, @scope, attribute, value)
|
61
|
+
else
|
62
|
+
raise Errors::AdapterNotImplemented.new \
|
63
|
+
resource.adapter, attribute, method
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def each_filter
|
68
|
+
filter_param.each_pair do |param_name, param_value|
|
69
|
+
filter = find_filter!(param_name)
|
70
|
+
param_value = { eq: param_value } unless param_value.is_a?(Hash)
|
71
|
+
value = param_value.values.first
|
72
|
+
operator = param_value.keys.first
|
73
|
+
value = param_value.values.first unless filter.values[0][:type] == :hash
|
74
|
+
value = value.split(',') if value.is_a?(String) && value.include?(',')
|
75
|
+
value = coerce_types(param_name.to_sym, value)
|
76
|
+
yield filter, operator, value
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def coerce_types(name, value)
|
81
|
+
type_name = @resource.all_attributes[name][:type]
|
82
|
+
is_array = type_name.to_s.starts_with?('array_of') ||
|
83
|
+
Types[type_name][:canonical_name] == :array
|
84
|
+
|
85
|
+
if is_array
|
86
|
+
@resource.typecast(name, value, :filterable)
|
87
|
+
else
|
88
|
+
value = value.nil? || value.is_a?(Hash) ? [value] : Array(value)
|
89
|
+
value.map { |v| @resource.typecast(name, v, :filterable) }
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Graphiti
|
2
|
+
# @api private
|
3
|
+
module Scoping::Filterable
|
4
|
+
# @api private
|
5
|
+
def find_filter(name)
|
6
|
+
find_filter!(name)
|
7
|
+
rescue Graphiti::Errors::AttributeError
|
8
|
+
nil
|
9
|
+
end
|
10
|
+
|
11
|
+
# @api private
|
12
|
+
def find_filter!(name)
|
13
|
+
resource.class.get_attr!(name, :filterable, request: true)
|
14
|
+
{ name => resource.filters[name] }
|
15
|
+
end
|
16
|
+
|
17
|
+
# @api private
|
18
|
+
def filter_param
|
19
|
+
query_hash[:filter] || {}
|
20
|
+
end
|
21
|
+
|
22
|
+
def missing_required_filters
|
23
|
+
required_attributes - filter_param.keys
|
24
|
+
end
|
25
|
+
|
26
|
+
def required_attributes
|
27
|
+
resource.attributes.map do |k, v|
|
28
|
+
k if v[:filterable] == :required
|
29
|
+
end.compact
|
30
|
+
end
|
31
|
+
|
32
|
+
def required_filters_provided?
|
33
|
+
missing_required_filters.empty?
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|