jsonapi_compliable 0.11.34 → 1.0.alpha.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.ruby-version +1 -1
- data/.travis.yml +1 -2
- data/Rakefile +7 -3
- data/jsonapi_compliable.gemspec +7 -3
- data/lib/generators/jsonapi/resource_generator.rb +8 -79
- data/lib/generators/jsonapi/templates/application_resource.rb.erb +2 -1
- data/lib/generators/jsonapi/templates/controller.rb.erb +19 -64
- data/lib/generators/jsonapi/templates/resource.rb.erb +5 -47
- 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/jsonapi_compliable.rb +87 -18
- data/lib/jsonapi_compliable/adapters/abstract.rb +202 -45
- data/lib/jsonapi_compliable/adapters/active_record.rb +6 -130
- data/lib/jsonapi_compliable/adapters/active_record/base.rb +247 -0
- data/lib/jsonapi_compliable/adapters/active_record/belongs_to_sideload.rb +17 -0
- data/lib/jsonapi_compliable/adapters/active_record/has_many_sideload.rb +17 -0
- data/lib/jsonapi_compliable/adapters/active_record/has_one_sideload.rb +17 -0
- data/lib/jsonapi_compliable/adapters/active_record/inferrence.rb +12 -0
- data/lib/jsonapi_compliable/adapters/active_record/many_to_many_sideload.rb +30 -0
- data/lib/jsonapi_compliable/adapters/null.rb +177 -6
- data/lib/jsonapi_compliable/base.rb +33 -320
- data/lib/jsonapi_compliable/context.rb +16 -0
- data/lib/jsonapi_compliable/deserializer.rb +14 -39
- data/lib/jsonapi_compliable/errors.rb +227 -24
- data/lib/jsonapi_compliable/extensions/extra_attribute.rb +3 -1
- data/lib/jsonapi_compliable/filter_operators.rb +25 -0
- data/lib/jsonapi_compliable/hash_renderer.rb +57 -0
- data/lib/jsonapi_compliable/query.rb +190 -202
- data/lib/jsonapi_compliable/rails.rb +12 -6
- data/lib/jsonapi_compliable/railtie.rb +64 -0
- data/lib/jsonapi_compliable/renderer.rb +60 -0
- data/lib/jsonapi_compliable/resource.rb +35 -663
- data/lib/jsonapi_compliable/resource/configuration.rb +239 -0
- data/lib/jsonapi_compliable/resource/dsl.rb +138 -0
- data/lib/jsonapi_compliable/resource/interface.rb +32 -0
- data/lib/jsonapi_compliable/resource/polymorphism.rb +68 -0
- data/lib/jsonapi_compliable/resource/sideloading.rb +102 -0
- data/lib/jsonapi_compliable/resource_proxy.rb +127 -0
- data/lib/jsonapi_compliable/responders.rb +19 -0
- data/lib/jsonapi_compliable/runner.rb +25 -0
- data/lib/jsonapi_compliable/scope.rb +37 -79
- data/lib/jsonapi_compliable/scoping/extra_attributes.rb +29 -0
- data/lib/jsonapi_compliable/scoping/filter.rb +39 -58
- data/lib/jsonapi_compliable/scoping/filterable.rb +9 -14
- data/lib/jsonapi_compliable/scoping/paginate.rb +9 -3
- data/lib/jsonapi_compliable/scoping/sort.rb +16 -4
- data/lib/jsonapi_compliable/sideload.rb +221 -347
- data/lib/jsonapi_compliable/sideload/belongs_to.rb +34 -0
- data/lib/jsonapi_compliable/sideload/has_many.rb +16 -0
- data/lib/jsonapi_compliable/sideload/has_one.rb +9 -0
- data/lib/jsonapi_compliable/sideload/many_to_many.rb +24 -0
- data/lib/jsonapi_compliable/sideload/polymorphic_belongs_to.rb +108 -0
- data/lib/jsonapi_compliable/stats/payload.rb +4 -8
- data/lib/jsonapi_compliable/types.rb +172 -0
- data/lib/jsonapi_compliable/util/attribute_check.rb +88 -0
- data/lib/jsonapi_compliable/util/persistence.rb +29 -7
- data/lib/jsonapi_compliable/util/relationship_payload.rb +4 -4
- data/lib/jsonapi_compliable/util/render_options.rb +4 -32
- data/lib/jsonapi_compliable/util/serializer_attributes.rb +98 -0
- data/lib/jsonapi_compliable/util/validation_response.rb +15 -9
- data/lib/jsonapi_compliable/version.rb +1 -1
- metadata +105 -24
- data/lib/generators/jsonapi/field_generator.rb +0 -0
- data/lib/generators/jsonapi/templates/create_request_spec.rb.erb +0 -29
- data/lib/generators/jsonapi/templates/destroy_request_spec.rb.erb +0 -20
- data/lib/generators/jsonapi/templates/index_request_spec.rb.erb +0 -22
- data/lib/generators/jsonapi/templates/payload.rb.erb +0 -39
- data/lib/generators/jsonapi/templates/serializer.rb.erb +0 -25
- data/lib/generators/jsonapi/templates/show_request_spec.rb.erb +0 -19
- data/lib/generators/jsonapi/templates/update_request_spec.rb.erb +0 -33
- data/lib/jsonapi_compliable/adapters/active_record_sideloading.rb +0 -152
- data/lib/jsonapi_compliable/scoping/extra_fields.rb +0 -58
@@ -28,9 +28,12 @@ module JsonapiCompliable
|
|
28
28
|
# aliases. If valid, call either the default or custom filtering logic.
|
29
29
|
# @return the scope we are chaining/modifying
|
30
30
|
def apply
|
31
|
-
|
32
|
-
|
33
|
-
|
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)
|
34
37
|
end
|
35
38
|
|
36
39
|
@scope
|
@@ -38,74 +41,52 @@ module JsonapiCompliable
|
|
38
41
|
|
39
42
|
private
|
40
43
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
if custom_scope = filter.values.first[
|
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]
|
45
48
|
custom_scope.call(@scope, value, resource.context)
|
46
49
|
else
|
47
|
-
|
50
|
+
filter_via_adapter(filter, operator, value)
|
48
51
|
end
|
49
52
|
end
|
50
53
|
|
51
|
-
def
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
value = parse_string_arrays(value)
|
56
|
-
value = normalize_string_values(value)
|
57
|
-
yield filter, value
|
58
|
-
end
|
59
|
-
end
|
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
|
60
58
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
# {{{ "id": 1 }}} becomes { 'id' => 1 }
|
67
|
-
def parse_string_arrays(value)
|
68
|
-
if value.is_a?(String)# && value[0..2] != '{{{'
|
69
|
-
# Escaped JSON
|
70
|
-
if value[0..2] == '{{{'
|
71
|
-
value = value.sub('{{', '').sub('}}', '')
|
72
|
-
value = JSON.parse(value)
|
73
|
-
else
|
74
|
-
# Find the quoted strings
|
75
|
-
quotes = value.scan(/{{.*?}}/)
|
76
|
-
# remove them from the rest
|
77
|
-
quotes.each { |q| value.gsub!(q, '') }
|
78
|
-
# remove the quote characters from the quoted strings
|
79
|
-
quotes.each { |q| q.gsub!('{{', '').gsub!('}}', '') }
|
80
|
-
# merge everything back together into an array
|
81
|
-
value = Array(value.split(',')) + quotes
|
82
|
-
# remove any blanks that are left
|
83
|
-
value.reject! { |v| v.length.zero? }
|
84
|
-
value = value[0] if value.length == 1
|
85
|
-
end
|
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
|
86
64
|
end
|
87
|
-
value
|
88
65
|
end
|
89
66
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
value.
|
97
|
-
|
98
|
-
|
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
|
99
77
|
end
|
100
78
|
end
|
101
79
|
|
102
|
-
def
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
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)
|
107
87
|
else
|
108
|
-
value
|
88
|
+
value = value.nil? || value.is_a?(Hash) ? [value] : Array(value)
|
89
|
+
value.map { |v| @resource.typecast(name, v, :filterable) }
|
109
90
|
end
|
110
91
|
end
|
111
92
|
end
|
@@ -4,34 +4,29 @@ module JsonapiCompliable
|
|
4
4
|
# @api private
|
5
5
|
def find_filter(name)
|
6
6
|
find_filter!(name)
|
7
|
-
rescue JsonapiCompliable::Errors::
|
7
|
+
rescue JsonapiCompliable::Errors::AttributeError
|
8
8
|
nil
|
9
9
|
end
|
10
10
|
|
11
11
|
# @api private
|
12
12
|
def find_filter!(name)
|
13
|
-
|
14
|
-
|
15
|
-
raise JsonapiCompliable::Errors::BadFilter unless filter_name
|
16
|
-
if guard = filter_value[:if]
|
17
|
-
raise JsonapiCompliable::Errors::BadFilter if resource.context.send(guard) == false
|
18
|
-
end
|
19
|
-
{ filter_name => filter_value }
|
13
|
+
resource.class.get_attr!(name, :filterable, request: true)
|
14
|
+
{ name => resource.filters[name] }
|
20
15
|
end
|
21
16
|
|
22
17
|
# @api private
|
23
18
|
def filter_param
|
24
|
-
query_hash[:filter]
|
19
|
+
query_hash[:filter] || {}
|
25
20
|
end
|
26
21
|
|
27
22
|
def missing_required_filters
|
28
|
-
|
23
|
+
required_attributes - filter_param.keys
|
29
24
|
end
|
30
25
|
|
31
|
-
def
|
32
|
-
resource.
|
33
|
-
|
34
|
-
end
|
26
|
+
def required_attributes
|
27
|
+
resource.attributes.map do |k, v|
|
28
|
+
k if v[:filterable] == :required
|
29
|
+
end.compact
|
35
30
|
end
|
36
31
|
|
37
32
|
def required_filters_provided?
|
@@ -31,6 +31,8 @@ module JsonapiCompliable
|
|
31
31
|
if size > MAX_PAGE_SIZE
|
32
32
|
raise JsonapiCompliable::Errors::UnsupportedPageSize
|
33
33
|
.new(size, MAX_PAGE_SIZE)
|
34
|
+
elsif requested? && @opts[:sideload_parent_length].to_i > 1
|
35
|
+
raise JsonapiCompliable::Errors::UnsupportedPagination
|
34
36
|
else
|
35
37
|
super
|
36
38
|
end
|
@@ -42,8 +44,8 @@ module JsonapiCompliable
|
|
42
44
|
#
|
43
45
|
# @return [Boolean] should we apply this logic?
|
44
46
|
def apply?
|
45
|
-
if @opts[:
|
46
|
-
|
47
|
+
if @opts[:default_paginate] == false
|
48
|
+
requested?
|
47
49
|
else
|
48
50
|
true
|
49
51
|
end
|
@@ -66,12 +68,16 @@ module JsonapiCompliable
|
|
66
68
|
|
67
69
|
private
|
68
70
|
|
71
|
+
def requested?
|
72
|
+
not [page_param[:size], page_param[:number]].all?(&:nil?)
|
73
|
+
end
|
74
|
+
|
69
75
|
def page_param
|
70
76
|
@page_param ||= (query_hash[:page] || {})
|
71
77
|
end
|
72
78
|
|
73
79
|
def number
|
74
|
-
(page_param[:number] ||
|
80
|
+
(page_param[:number] || 1).to_i
|
75
81
|
end
|
76
82
|
|
77
83
|
def size
|
@@ -15,14 +15,18 @@ module JsonapiCompliable
|
|
15
15
|
class Scoping::Sort < Scoping::Base
|
16
16
|
# @return [Proc, Nil] The custom proc specified by Resource DSL
|
17
17
|
def custom_scope
|
18
|
-
resource.
|
18
|
+
resource.sort_all
|
19
19
|
end
|
20
20
|
|
21
21
|
# Apply default scope logic via Resource adapter
|
22
22
|
# @return the scope we are chaining/modifying
|
23
23
|
def apply_standard_scope
|
24
24
|
each_sort do |attribute, direction|
|
25
|
-
|
25
|
+
if sort = resource.sorts[attribute]
|
26
|
+
@scope = sort.call(@scope, direction)
|
27
|
+
else
|
28
|
+
@scope = resource.adapter.order(@scope, attribute, direction)
|
29
|
+
end
|
26
30
|
end
|
27
31
|
@scope
|
28
32
|
end
|
@@ -41,12 +45,20 @@ module JsonapiCompliable
|
|
41
45
|
|
42
46
|
def each_sort
|
43
47
|
sort_param.each do |sort_hash|
|
44
|
-
|
48
|
+
attribute = sort_hash.keys.first
|
49
|
+
direction = sort_hash.values.first
|
50
|
+
yield attribute, direction
|
45
51
|
end
|
46
52
|
end
|
47
53
|
|
48
54
|
def sort_param
|
49
|
-
@sort_param ||=
|
55
|
+
@sort_param ||= begin
|
56
|
+
if query_hash[:sort].blank?
|
57
|
+
resource.default_sort
|
58
|
+
else
|
59
|
+
query_hash[:sort]
|
60
|
+
end
|
61
|
+
end
|
50
62
|
end
|
51
63
|
end
|
52
64
|
end
|
@@ -1,220 +1,179 @@
|
|
1
1
|
module JsonapiCompliable
|
2
|
-
# @attr_reader [Symbol] name The name of the sideload
|
3
|
-
# @attr_reader [Class] resource_class The corresponding Resource class
|
4
|
-
# @attr_reader [Boolean] polymorphic Is this a polymorphic sideload?
|
5
|
-
# @attr_reader [Hash] polymorphic_groups The subgroups, when polymorphic
|
6
|
-
# @attr_reader [Hash] sideloads The associated sibling sideloads
|
7
|
-
# @attr_reader [Proc] scope_proc The configured 'scope' block
|
8
|
-
# @attr_reader [Proc] assign_proc The configured 'assign' block
|
9
|
-
# @attr_reader [Symbol] grouping_field The configured 'group_by' symbol
|
10
|
-
# @attr_reader [Symbol] foreign_key The attribute used to match objects - need not be a true database foreign key.
|
11
|
-
# @attr_reader [Symbol] primary_key The attribute used to match objects - need not be a true database primary key.
|
12
|
-
# @attr_reader [Symbol] type One of :has_many, :belongs_to, etc
|
13
2
|
class Sideload
|
3
|
+
HOOK_ACTIONS = [:save, :create, :update, :destroy, :disassociate]
|
4
|
+
TYPES = [:has_many, :belongs_to, :has_one, :many_to_many]
|
5
|
+
|
14
6
|
attr_reader :name,
|
15
7
|
:resource_class,
|
16
|
-
:
|
17
|
-
:polymorphic_groups,
|
18
|
-
:parent,
|
19
|
-
:sideloads,
|
20
|
-
:scope_proc,
|
21
|
-
:assign_proc,
|
22
|
-
:grouping_field,
|
8
|
+
:parent_resource_class,
|
23
9
|
:foreign_key,
|
24
10
|
:primary_key,
|
25
|
-
:
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
@
|
36
|
-
@
|
37
|
-
@
|
38
|
-
@
|
39
|
-
@
|
40
|
-
@
|
41
|
-
@
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
11
|
+
:parent,
|
12
|
+
:group_name
|
13
|
+
|
14
|
+
class_attribute :scope_proc,
|
15
|
+
:assign_proc,
|
16
|
+
:assign_each_proc,
|
17
|
+
:params_proc,
|
18
|
+
:pre_load_proc
|
19
|
+
|
20
|
+
def initialize(name, opts)
|
21
|
+
@name = name
|
22
|
+
@parent_resource_class = opts[:parent_resource]
|
23
|
+
@resource_class = opts[:resource]
|
24
|
+
@primary_key = opts[:primary_key]
|
25
|
+
@foreign_key = opts[:foreign_key]
|
26
|
+
@type = opts[:type]
|
27
|
+
@base_scope = opts[:base_scope]
|
28
|
+
@readable = opts[:readable]
|
29
|
+
@writable = opts[:writable]
|
30
|
+
@as = opts[:as]
|
31
|
+
|
32
|
+
# polymorphic-specific
|
33
|
+
@group_name = opts[:group_name]
|
34
|
+
@polymorphic_child = opts[:polymorphic_child]
|
35
|
+
@parent = opts[:parent]
|
36
|
+
if polymorphic_child?
|
37
|
+
parent.resource.polymorphic << resource_class
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.scope(&blk)
|
42
|
+
self.scope_proc = blk
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.assign(&blk)
|
46
|
+
self.assign_proc = blk
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.assign_each(&blk)
|
50
|
+
self.assign_each_proc = blk
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.params(&blk)
|
54
|
+
self.params_proc = blk
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.pre_load(&blk)
|
58
|
+
self.pre_load_proc = blk
|
59
|
+
end
|
60
|
+
|
61
|
+
def readable?
|
62
|
+
!!@readable
|
63
|
+
end
|
64
|
+
|
65
|
+
def writable?
|
66
|
+
!!@writable
|
67
|
+
end
|
68
|
+
|
69
|
+
def polymorphic_parent?
|
70
|
+
resource.polymorphic?
|
71
|
+
end
|
72
|
+
|
73
|
+
def polymorphic_child?
|
74
|
+
!!@polymorphic_child
|
75
|
+
end
|
76
|
+
|
77
|
+
def primary_key
|
78
|
+
@primary_key ||= :id
|
79
|
+
end
|
80
|
+
|
81
|
+
def foreign_key
|
82
|
+
@foreign_key ||= infer_foreign_key
|
83
|
+
end
|
84
|
+
|
85
|
+
def association_name
|
86
|
+
@as || name
|
87
|
+
end
|
88
|
+
|
89
|
+
def resource_class
|
90
|
+
@resource_class ||= infer_resource_class
|
91
|
+
end
|
92
|
+
|
93
|
+
def scope(parents)
|
94
|
+
raise "No #scope defined for sideload with name '#{name}'. Make sure to define this in your adapter, or pass a block that defines the scope."
|
95
|
+
end
|
96
|
+
|
97
|
+
def assign_each(parent, children)
|
98
|
+
raise 'Override #assign_each in subclass'
|
99
|
+
end
|
100
|
+
|
101
|
+
def type
|
102
|
+
@type || raise("Override #type in subclass. Should be one of #{TYPES.inspect}")
|
103
|
+
end
|
104
|
+
|
105
|
+
def load_params(parents, query)
|
106
|
+
raise 'Override #load_params in subclass'
|
107
|
+
end
|
108
|
+
|
109
|
+
def base_scope
|
110
|
+
if @base_scope
|
111
|
+
@base_scope.respond_to?(:call) ? @base_scope.call : @base_scope
|
112
|
+
else
|
113
|
+
resource.base_scope
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def load(parents, query)
|
118
|
+
params = load_params(parents, query)
|
119
|
+
params_proc.call(params, parents, query) if params_proc
|
120
|
+
opts = load_options(parents, query)
|
121
|
+
proxy = resource.class._all(params, opts, base_scope)
|
122
|
+
pre_load_proc.call(proxy) if pre_load_proc
|
123
|
+
proxy.to_a
|
124
|
+
end
|
125
|
+
|
126
|
+
# Override in subclass
|
127
|
+
def infer_foreign_key
|
128
|
+
model = parent_resource_class.model
|
129
|
+
namespace = namespace_for(model)
|
130
|
+
model_name = model.name.gsub("#{namespace}::", '')
|
131
|
+
:"#{model_name.underscore}_id"
|
132
|
+
end
|
133
|
+
|
48
134
|
def resource
|
49
135
|
@resource ||= resource_class.new
|
50
136
|
end
|
51
137
|
|
52
|
-
|
53
|
-
|
54
|
-
# Polymorphic sideloads group the parent objects in some fashion,
|
55
|
-
# so different 'types' can be resolved differently. Let's say an
|
56
|
-
# +Office+ has a polymorphic +Organization+, which can be either a
|
57
|
-
# +Business+ or +Government+:
|
58
|
-
#
|
59
|
-
# allow_sideload :organization, :polymorphic: true do
|
60
|
-
# group_by :organization_type
|
61
|
-
#
|
62
|
-
# allow_sideload 'Business', resource: BusinessResource do
|
63
|
-
# # ... code ...
|
64
|
-
# end
|
65
|
-
#
|
66
|
-
# allow_sideload 'Governemnt', resource: GovernmentResource do
|
67
|
-
# # ... code ...
|
68
|
-
# end
|
69
|
-
# end
|
70
|
-
#
|
71
|
-
# You probably want to extract this code into an Adapter. For instance,
|
72
|
-
# with ActiveRecord:
|
73
|
-
#
|
74
|
-
# polymorphic_belongs_to :organization,
|
75
|
-
# group_by: :organization_type,
|
76
|
-
# groups: {
|
77
|
-
# 'Business' => {
|
78
|
-
# scope: -> { Business.all },
|
79
|
-
# resource: BusinessResource,
|
80
|
-
# foreign_key: :organization_id
|
81
|
-
# },
|
82
|
-
# 'Government' => {
|
83
|
-
# scope: -> { Government.all },
|
84
|
-
# resource: GovernmentResource,
|
85
|
-
# foreign_key: :organization_id
|
86
|
-
# }
|
87
|
-
# }
|
88
|
-
#
|
89
|
-
# @see Adapters::ActiveRecordSideloading#polymorphic_belongs_to
|
90
|
-
# @return [Boolean] is this sideload polymorphic?
|
91
|
-
def polymorphic?
|
92
|
-
@polymorphic == true
|
93
|
-
end
|
94
|
-
|
95
|
-
# Build a scope that will be used to fetch the related records
|
96
|
-
# This scope will be further chained with filtering/sorting/etc
|
97
|
-
#
|
98
|
-
# You probably want to wrap this logic in an Adapter, instead of
|
99
|
-
# specifying in your resource directly.
|
100
|
-
#
|
101
|
-
# @example Default ActiveRecord
|
102
|
-
# class PostResource < ApplicationResource
|
103
|
-
# # ... code ...
|
104
|
-
# allow_sideload :comments, resource: CommentResource do
|
105
|
-
# scope do |posts|
|
106
|
-
# Comment.where(post_id: posts.map(&:id))
|
107
|
-
# end
|
108
|
-
# # ... code ...
|
109
|
-
# end
|
110
|
-
# end
|
111
|
-
#
|
112
|
-
# @example Custom Scope
|
113
|
-
# # In this example, our base scope is a Hash
|
114
|
-
# scope do |posts|
|
115
|
-
# { post_ids: posts.map(&:id) }
|
116
|
-
# end
|
117
|
-
#
|
118
|
-
# @example ActiveRecord via Adapter
|
119
|
-
# class PostResource < ApplicationResource
|
120
|
-
# # ... code ...
|
121
|
-
# has_many :comments,
|
122
|
-
# scope: -> { Comment.all },
|
123
|
-
# resource: CommentResource,
|
124
|
-
# foreign_key: :post_id
|
125
|
-
# end
|
126
|
-
#
|
127
|
-
# @see Adapters::Abstract
|
128
|
-
# @see Adapters::ActiveRecordSideloading#has_many
|
129
|
-
# @see #allow_sideload
|
130
|
-
# @yieldparam parents - The resolved parent records
|
131
|
-
def scope(&blk)
|
132
|
-
@scope_proc = blk
|
133
|
-
end
|
134
|
-
|
135
|
-
# The proc used to assign the resolved parents and children.
|
136
|
-
#
|
137
|
-
# You probably want to wrap this logic in an Adapter, instead of
|
138
|
-
# specifying in your resource directly.
|
139
|
-
#
|
140
|
-
# @example Default ActiveRecord
|
141
|
-
# class PostResource < ApplicationResource
|
142
|
-
# # ... code ...
|
143
|
-
# allow_sideload :comments, resource: CommentResource do
|
144
|
-
# # ... code ...
|
145
|
-
# assign do |posts, comments|
|
146
|
-
# posts.each do |post|
|
147
|
-
# relevant_comments = comments.select { |c| c.post_id == post.id }
|
148
|
-
# post.comments = relevant_comments
|
149
|
-
# end
|
150
|
-
# end
|
151
|
-
# end
|
152
|
-
# end
|
153
|
-
#
|
154
|
-
# @example ActiveRecord via Adapter
|
155
|
-
# class PostResource < ApplicationResource
|
156
|
-
# # ... code ...
|
157
|
-
# has_many :comments,
|
158
|
-
# scope: -> { Comment.all },
|
159
|
-
# resource: CommentResource,
|
160
|
-
# foreign_key: :post_id
|
161
|
-
# end
|
162
|
-
#
|
163
|
-
# @see Adapters::Abstract
|
164
|
-
# @see Adapters::ActiveRecordSideloading#has_many
|
165
|
-
# @see #allow_sideload
|
166
|
-
# @yieldparam parents - The resolved parent records
|
167
|
-
# @yieldparam children - The resolved child records
|
168
|
-
def assign(&blk)
|
169
|
-
@assign_proc = blk
|
170
|
-
end
|
171
|
-
|
172
|
-
# Configure how to associate parent and child records.
|
173
|
-
# Delegates to #resource
|
174
|
-
#
|
175
|
-
# @see #name
|
176
|
-
# @see #type
|
177
|
-
# @api private
|
178
|
-
def associate(parent, child)
|
179
|
-
association_name = @parent ? @parent.name : name
|
180
|
-
resource.associate(parent, child, association_name, type)
|
138
|
+
def parent_resource
|
139
|
+
@parent_resource ||= parent_resource_class.new
|
181
140
|
end
|
182
141
|
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
142
|
+
def assign(parents, children)
|
143
|
+
associated = []
|
144
|
+
parents.each do |parent|
|
145
|
+
relevant_children = fire_assign_each(parent, children)
|
146
|
+
if relevant_children.is_a?(Array)
|
147
|
+
associated |= relevant_children
|
148
|
+
associate_all(parent, relevant_children)
|
149
|
+
else
|
150
|
+
associated << relevant_children
|
151
|
+
associate(parent, relevant_children)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
(children - associated).each do |unassigned|
|
155
|
+
children.delete(unassigned)
|
156
|
+
end
|
192
157
|
end
|
193
158
|
|
194
|
-
|
159
|
+
def resolve(parents, query)
|
160
|
+
# legacy / custom / many-to-many
|
161
|
+
if self.class.scope_proc || type == :many_to_many
|
162
|
+
sideload_scope = fire_scope(parents)
|
163
|
+
sideload_scope = Scope.new sideload_scope,
|
164
|
+
resource,
|
165
|
+
query,
|
166
|
+
sideload_parent_length: parents.length,
|
167
|
+
default_paginate: false
|
168
|
+
sideload_scope.resolve do |sideload_results|
|
169
|
+
fire_assign(parents, sideload_results)
|
170
|
+
end
|
171
|
+
else
|
172
|
+
load(parents, query)
|
173
|
+
end
|
174
|
+
end
|
195
175
|
|
196
|
-
|
197
|
-
#
|
198
|
-
# In particular, helpful for bulk operations. "after_save" will fire
|
199
|
-
# for any persistence method - +:create+, +:update+, +:destroy+, +:disassociate+.
|
200
|
-
# Use "only" and "except" keyword arguments to fire only for a
|
201
|
-
# specific persistence method.
|
202
|
-
#
|
203
|
-
# @example Bulk Notify Users on Invite
|
204
|
-
# class ProjectResource < ApplicationResource
|
205
|
-
# # ... code ...
|
206
|
-
# allow_sideload :users, resource: UserResource do
|
207
|
-
# # scope {}
|
208
|
-
# # assign {}
|
209
|
-
# after_save only: [:create] do |project, users|
|
210
|
-
# UserMailer.invite(project, users).deliver_later
|
211
|
-
# end
|
212
|
-
# end
|
213
|
-
# end
|
214
|
-
#
|
215
|
-
# @see #hooks
|
216
|
-
# @see Util::Persistence
|
217
|
-
def after_save(only: [], except: [], &blk)
|
176
|
+
def self.after_save(only: [], except: [], &blk)
|
218
177
|
actions = HOOK_ACTIONS - except
|
219
178
|
actions = only & actions
|
220
179
|
actions = [:save] if only.empty? && except.empty?
|
@@ -223,10 +182,7 @@ module JsonapiCompliable
|
|
223
182
|
end
|
224
183
|
end
|
225
184
|
|
226
|
-
|
227
|
-
# @see #after_save
|
228
|
-
# @return hash of hooks, ie +{ after_create: #<Proc>}+
|
229
|
-
def hooks
|
185
|
+
def self.hooks
|
230
186
|
@hooks ||= {}.tap do |h|
|
231
187
|
HOOK_ACTIONS.each do |a|
|
232
188
|
h[:"after_#{a}"] = []
|
@@ -235,173 +191,91 @@ module JsonapiCompliable
|
|
235
191
|
end
|
236
192
|
end
|
237
193
|
|
238
|
-
|
239
|
-
|
240
|
-
# and +parent_type+. We would want to group on +parent_type+:
|
241
|
-
#
|
242
|
-
# allow_sideload :organization, polymorphic: true do
|
243
|
-
# # group parent_type, parent here is 'organization'
|
244
|
-
# group_by :organization_type
|
245
|
-
# end
|
246
|
-
#
|
247
|
-
# @see #polymorphic?
|
248
|
-
def group_by(grouping_field)
|
249
|
-
@grouping_field = grouping_field
|
250
|
-
end
|
251
|
-
|
252
|
-
# Resolve the sideload.
|
253
|
-
#
|
254
|
-
# * Uses the 'scope' proc to build a 'base scope'
|
255
|
-
# * Chains additional criteria onto that 'base scope'
|
256
|
-
# * Resolves that scope (see Scope#resolve)
|
257
|
-
# * Assigns the resulting child objects to their corresponding parents
|
258
|
-
#
|
259
|
-
# @see Scope#resolve
|
260
|
-
# @param [Object] parents The resolved parent models
|
261
|
-
# @param [Query] query The Query instance
|
262
|
-
# @param [Symbol] namespace The current namespace (see Resource#with_context)
|
263
|
-
# @see Query
|
264
|
-
# @see Resource#with_context
|
265
|
-
# @return [void]
|
266
|
-
# @api private
|
267
|
-
def resolve(parents, query, namespace)
|
268
|
-
if polymorphic?
|
269
|
-
resolve_polymorphic(parents, query)
|
270
|
-
else
|
271
|
-
resolve_basic(parents, query, namespace)
|
272
|
-
end
|
273
|
-
end
|
194
|
+
def fire_hooks!(parent, objects, method)
|
195
|
+
return unless self.class.hooks
|
274
196
|
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
# rather than using directly
|
279
|
-
#
|
280
|
-
# @example Default ActiveRecord
|
281
|
-
# # What happens 'under the hood'
|
282
|
-
# class CommentResource < ApplicationResource
|
283
|
-
# # ... code ...
|
284
|
-
# allow_sideload :post, resource: PostResource do
|
285
|
-
# scope do |comments|
|
286
|
-
# Post.where(id: comments.map(&:post_id))
|
287
|
-
# end
|
288
|
-
#
|
289
|
-
# assign do |comments, posts|
|
290
|
-
# comments.each do |comment|
|
291
|
-
# relevant_post = posts.find { |p| p.id == comment.post_id }
|
292
|
-
# comment.post = relevant_post
|
293
|
-
# end
|
294
|
-
# end
|
295
|
-
# end
|
296
|
-
# end
|
297
|
-
#
|
298
|
-
# # Rather than writing that code directly, go through the adapter:
|
299
|
-
# class CommentResource < ApplicationResource
|
300
|
-
# # ... code ...
|
301
|
-
# use_adapter JsonapiCompliable::Adapters::ActiveRecord
|
302
|
-
#
|
303
|
-
# belongs_to :post,
|
304
|
-
# scope: -> { Post.all },
|
305
|
-
# resource: PostResource,
|
306
|
-
# foreign_key: :post_id
|
307
|
-
# end
|
308
|
-
#
|
309
|
-
# @see Adapters::ActiveRecordSideloading#belongs_to
|
310
|
-
# @see #assign
|
311
|
-
# @see #scope
|
312
|
-
# @return void
|
313
|
-
def allow_sideload(name, opts = {}, &blk)
|
314
|
-
sideload = Sideload.new(name, opts)
|
315
|
-
sideload.instance_eval(&blk) if blk
|
316
|
-
|
317
|
-
if polymorphic?
|
318
|
-
@polymorphic_groups[name] = sideload
|
319
|
-
else
|
320
|
-
@sideloads[name] = sideload
|
197
|
+
all = self.class.hooks[:"after_#{method}"] + self.class.hooks[:after_save]
|
198
|
+
all.compact.each do |hook|
|
199
|
+
resource.instance_exec(parent, objects, &hook)
|
321
200
|
end
|
322
201
|
end
|
323
202
|
|
324
|
-
|
325
|
-
|
326
|
-
# @see +allow_sideload
|
327
|
-
# @return the corresponding Sideload object
|
328
|
-
def sideload(name)
|
329
|
-
@sideloads[name]
|
203
|
+
def associate_all(parent, children)
|
204
|
+
parent_resource.associate_all(parent, children, association_name, type)
|
330
205
|
end
|
331
206
|
|
332
|
-
|
333
|
-
|
334
|
-
{}.tap do |all|
|
335
|
-
if polymorphic?
|
336
|
-
polymorphic_groups.each_pair do |type, sl|
|
337
|
-
all.merge!(sl.resource.sideloading.all_sideloads)
|
338
|
-
end
|
339
|
-
else
|
340
|
-
all.merge!(@sideloads.merge(resource.sideloading.sideloads))
|
341
|
-
end
|
342
|
-
end
|
207
|
+
def associate(parent, child)
|
208
|
+
parent_resource.associate(parent, child, association_name, type)
|
343
209
|
end
|
344
210
|
|
345
|
-
def
|
346
|
-
|
347
|
-
|
348
|
-
memo << sl.name
|
349
|
-
memo |= sl.association_names(memo)
|
350
|
-
end
|
351
|
-
end
|
211
|
+
def disassociate(parent, child)
|
212
|
+
parent_resource.disassociate(parent, child, association_name, type)
|
213
|
+
end
|
352
214
|
|
353
|
-
|
215
|
+
def ids_for_parents(parents)
|
216
|
+
parent_ids = parents.map(&primary_key)
|
217
|
+
parent_ids.compact!
|
218
|
+
parent_ids.uniq!
|
219
|
+
parent_ids
|
354
220
|
end
|
355
221
|
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
222
|
+
private
|
223
|
+
|
224
|
+
def load_options(parents, query)
|
225
|
+
{}.tap do |opts|
|
226
|
+
opts[:default_paginate] = false
|
227
|
+
opts[:sideload_parent_length] = parents.length
|
228
|
+
opts[:after_resolve] = ->(results) {
|
229
|
+
fire_assign(parents, results)
|
230
|
+
}
|
360
231
|
end
|
361
232
|
end
|
362
233
|
|
363
|
-
def
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
resource.instance_exec(parent, objects, &hook)
|
234
|
+
def fire_assign_each(parent, children)
|
235
|
+
if self.class.assign_each_proc
|
236
|
+
instance_exec(parent, children, &self.class.assign_each_proc)
|
237
|
+
else
|
238
|
+
assign_each(parent, children)
|
369
239
|
end
|
370
240
|
end
|
371
241
|
|
372
|
-
|
242
|
+
def fire_assign(parents, children)
|
243
|
+
if self.class.assign_proc
|
244
|
+
instance_exec(parents, children, &self.class.assign_proc)
|
245
|
+
else
|
246
|
+
assign(parents, children)
|
247
|
+
end
|
248
|
+
end
|
373
249
|
|
374
|
-
def
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
250
|
+
def fire_scope(parents)
|
251
|
+
parent_ids = ids_for_parents(parents)
|
252
|
+
if self.class.scope_proc
|
253
|
+
instance_exec(parent_ids, parents, &self.class.scope_proc)
|
254
|
+
else
|
255
|
+
method = method(:scope)
|
256
|
+
if [2,-2].include?(method.arity)
|
257
|
+
scope(parent_ids, parents)
|
382
258
|
else
|
383
|
-
|
259
|
+
scope(parent_ids)
|
384
260
|
end
|
385
261
|
end
|
386
262
|
end
|
387
263
|
|
388
|
-
def
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
sideload_for_group = @polymorphic_groups[group_type]
|
393
|
-
if sideload_for_group
|
394
|
-
sideload_for_group.resolve(group_members, query, name)
|
395
|
-
end
|
396
|
-
end
|
264
|
+
def namespace_for(klass)
|
265
|
+
namespace = klass.name
|
266
|
+
split = namespace.split('::')
|
267
|
+
split[0,split.length-1].join('::')
|
397
268
|
end
|
398
269
|
|
399
|
-
def
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
270
|
+
def infer_resource_class
|
271
|
+
namespace = namespace_for(parent_resource.class)
|
272
|
+
inferred_name = "#{name.to_s.singularize.classify}Resource"
|
273
|
+
klass = "#{namespace}::#{inferred_name}".safe_constantize
|
274
|
+
klass ||= inferred_name.safe_constantize
|
275
|
+
unless klass
|
276
|
+
raise Errors::ResourceNotFound.new(parent_resource, name)
|
404
277
|
end
|
278
|
+
klass
|
405
279
|
end
|
406
280
|
end
|
407
281
|
end
|